{
    "version": "https://jsonfeed.org/version/1",
    "title": "WiLine Edge Cloud — AI Tutorials",
    "home_page_url": "https://development-wec.wiline.com/docs/es/tutorials/",
    "description": "Tested, reproducible tutorials for self-hosting AI infrastructure on WiLine.",
    "items": [
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__authentik\" src=\"data:image/png;base64,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\" alt=\"authentik\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__mcp\" src=\"data:image/png;base64,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\" alt=\"Model Context Protocol\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Lock down Docker networks</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run containers as non-root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Membership, not just an account</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">An identity for the agent, not a key</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">A tool server that checks who is asking</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Where the agent can go, not just what it can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/\"><span class=\"skillTracker__dot\" data-state=\"locked\">8</span><span class=\"skillTracker__skill\" data-state=\"locked\">Move the daemon off root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/\"><span class=\"skillTracker__dot\" data-state=\"locked\">9</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run the model's own code without trusting it</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">🏆</span><span class=\"skillTracker__skill\" data-state=\"current\">A scope per tool, and a refusal clients can act on</span></span></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>La <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/\">parte 6</a> puso <code>required_scopes=[\"mcp:invoke\"]</code> en el\nverificador de JWT y dio por autorizado el servidor. Lo está, en el sentido de que quien llama\nsin autenticarse no obtiene nada. No lo está, en el sentido que importa: <code>mcp:invoke</code> abre todas\nlas herramientas del servidor. El mismo token que deja a un agente ejecutar <code>find_customer</code> le\ndeja ejecutar <code>issue_refund</code>.</p>\n<p>Ese es todo el hueco. Un agente de solo lectura y un agente que emite reembolsos tienen\ncredenciales idénticas, y lo único que se interpone entre \"busca a Maria\" y \"reembolsa la factura\n2\" es que nadie lo pidió.</p>\n<p>Este post lo cierra, primero mal y luego bien, porque el \"mal\" es lo que la mayoría despliega y la\ndiferencia solo se nota en lo que el cliente puede <em>hacer</em> ante un rechazo.</p>\n<p>Ambas versiones rechazan la misma llamada — un token de solo lectura pidiendo <code>issue_refund</code>. Se\ndiferencian en <em>dónde</em> se comprueba el ámbito, y esa única decisión determina lo que recibe el\ncliente:</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<p>Las partes 5 y 6, en concreto:</p>\n<ul>\n<li class=\"\">Authentik emitiendo tokens de client-credentials a un proveedor <code>agent-tools</code>\n(<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/\">parte 5</a>)</li>\n<li class=\"\">Un servidor FastMCP verificando esos tokens contra el endpoint JWKS\n(<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/\">parte 6</a>)</li>\n<li class=\"\"><code>~/mcp-auth/token.sh</code>, que recibe una cadena de ámbitos y devuelve un token de acceso</li>\n</ul>\n<p>El servidor de la parte 6 se queda en <code>:8770</code> todo el rato. Las dos versiones de abajo corren en\n<code>:8771</code> y <code>:8772</code> para que puedas comparar las tres sin parar nada.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--dos-ámbitos-nuevos\">Paso 1 — Dos ámbitos nuevos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/#paso-1--dos-%C3%A1mbitos-nuevos\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Dos ámbitos nuevos\" title=\"Enlace directo al Paso 1 — Dos ámbitos nuevos\" translate=\"no\">​</a></h2>\n<p>Los ámbitos son Property Mappings en Authentik. <strong>Customization → Property Mappings → New Property\nMapping → Scope Mapping</strong>, dos veces:</p>\n<table><thead><tr><th>Nombre</th><th>Nombre del ámbito</th><th>Descripción</th></tr></thead><tbody><tr><td><code>office-read</code></td><td><code>office:read</code></td><td>Consultar clientes y facturas</td></tr><tr><td><code>office-refund</code></td><td><code>office:refund</code></td><td>Emitir reembolsos</td></tr></tbody></table>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Dos nuevos mapeos de ámbito junto a los existentes gateway-invoke y mcp-invoke\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h10-scope-mappings-c80d52648d4347ac31467ecd7bfbc924.png\" width=\"1901\" height=\"831\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Crearlos no basta. Un proveedor solo emitirá un ámbito que se le haya asignado, así que abre\n<strong>Applications → Providers → agent-tools → Edit</strong> y mueve ambos a <em>Selected Scopes</em>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El proveedor agent-tools con mcp-invoke, office-read y office-refund seleccionados\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h10-provider-scopes-845343ec44444c00ad1c5931086ff83c.png\" width=\"1902\" height=\"944\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Fíjate en la línea bajo el selector: <em>\"Select which scopes can be used by the client. The client\nstill has to specify the scope to access the data.\"</em> Las dos mitades importan. Seleccionar un\námbito aquí no lo mete en todos los tokens — permite que el cliente lo pida. Un cliente que no pide\nnada no obtiene nada, y por eso cada llamada a <code>token.sh</code> de abajo pasa una cadena de ámbitos\nexplícita.</p>\n<p>Confirma que el token lleva de verdad lo que pediste:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/mcp-auth/token.sh </span><span class=\"token string\" style=\"color:#e3116c\">\"mcp:invoke office:read\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">cut</span><span class=\"token plain\"> -d. </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-f2</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> base64 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq .scope</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">\"mcp:invoke office:read\"</span><br></div></code></pre></div></div>\n<p>Si eso vuelve sin <code>office:read</code>, el ámbito no está en el proveedor — arréglalo antes de escribir\nuna línea de código de servidor, o te pasarás una hora depurando una aplicación de permisos que\nfunciona perfectamente sobre un token al que nunca se le asignó el ámbito.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--la-implementación-obvia\">Paso 2 — La implementación obvia<a href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/#paso-2--la-implementaci%C3%B3n-obvia\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — La implementación obvia\" title=\"Enlace directo al Paso 2 — La implementación obvia\" translate=\"no\">​</a></h2>\n<p>Mantén <code>required_scopes=[\"mcp:invoke\"]</code> en el verificador como precio de entrada, y luego pregunta\npor herramienta si quien llama tiene lo que <em>esa</em> operación necesita. FastMCP expone el token ya\nverificado a través de <code>get_access_token()</code>, así que un decorador puede leer los claims que el\nverificador ya comprobó:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastmcp</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">exceptions </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> ToolError</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastmcp</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">server</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">dependencies </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> get_access_token</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">requires</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">scope</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">decorate</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">fn</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@functools</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">wraps</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">fn</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">wrapper</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token operator\" style=\"color:#393A34\">*</span><span class=\"token plain\">args</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">**</span><span class=\"token plain\">kwargs</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            token </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> get_access_token</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            held </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token builtin\">set</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token builtin\">getattr</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">token</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"scopes\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> scope </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> held</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token keyword\" style=\"color:#00009f\">raise</span><span class=\"token plain\"> ToolError</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                    </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f'insufficient_scope: this tool requires \"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">scope</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"; '</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                    </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f'token carries </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation builtin\">sorted</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation\">held</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation\"> </span><span class=\"token string-interpolation interpolation keyword\" style=\"color:#00009f\">or</span><span class=\"token string-interpolation interpolation\"> </span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">\"nothing\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> fn</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token operator\" style=\"color:#393A34\">*</span><span class=\"token plain\">args</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">**</span><span class=\"token plain\">kwargs</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> wrapper</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> decorate</span><br></div></code></pre></div></div>\n<p>Y luego una línea por herramienta:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@mcp</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">tool</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@requires</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"office:read\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">find_customer</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">query</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@mcp</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">tool</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@requires</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"office:refund\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">issue_refund</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">invoice_id</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">int</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><br></div></code></pre></div></div>\n<p>Lánzalo en <code>:8771</code> y prueba ambas herramientas con ambos tokens:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Dos tokens, dos herramientas: cada token permite una y rechaza la otra\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h10-per-tool-enforcement-36365bd959bc324dd447cfdbe681525c.png\" width=\"845\" height=\"564\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">--- token: mcp:invoke + office:read ---</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  find_customer  : ALLOWED -&gt; [{'id': 1, 'name': 'Maria Alvarez', ...}]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  issue_refund   : REFUSED -&gt; insufficient_scope: this tool requires \"office:refund\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">--- token: mcp:invoke + office:refund ---</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  find_customer  : REFUSED -&gt; insufficient_scope: this tool requires \"office:read\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  issue_refund   : ALLOWED -&gt; Refunded 80.00 on invoice 2.</span><br></div></code></pre></div></div>\n<p>Eso es aplicación real de permisos. El token de reembolso no puede leer, el de lectura no puede\nreembolsar, y el rechazo nombra el ámbito que falta. Para muchos despliegues, aquí es donde se\npara.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--por-qué-ese-rechazo-no-basta\">Paso 3 — Por qué ese rechazo no basta<a href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/#paso-3--por-qu%C3%A9-ese-rechazo-no-basta\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — Por qué ese rechazo no basta\" title=\"Enlace directo al Paso 3 — Por qué ese rechazo no basta\" translate=\"no\">​</a></h2>\n<p>Mira la capa HTTP en lugar de la librería cliente.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">TOK</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/mcp-auth/token.sh </span><span class=\"token variable string\" style=\"color:#e3116c\">\"mcp:invoke office:read\"</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST http://127.0.0.1:8771/mcp </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$TOK</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Accept: application/json, text/event-stream\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"issue_refund\",\"arguments\":{\"invoice_id\":2}}}'</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La comprobación en el cuerpo de la herramienta devuelve HTTP 200 con el rechazo dentro del cuerpo\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h10-200-no-challenge-ec64c16f2444a9ed7bcacaed0d917f3e.png\" width=\"827\" height=\"346\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">HTTP/1.1 200 OK</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">content-type: text/event-stream</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">data: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"content\":[{\"text\":\"insufficient_scope: this tool</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">requires \\\"office:refund\\\"; token carries ['mcp:invoke', 'office:read']\",\"type\":\"text\"},</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">\"isError\":true}}</span><br></div></code></pre></div></div>\n<p><strong>HTTP 200.</strong> La petición tuvo éxito; la que declinó fue la <em>herramienta</em>. Esa es la semántica\ncorrecta de JSON-RPC y es inútil para un cliente OAuth.</p>\n<p>Un cliente OAuth que quiere escalar — volver al servidor de autorización y pedir <code>office:refund</code> —\nestá atento a un <code>401</code> o <code>403</code> con una cabecera <code>WWW-Authenticate</code> que le diga qué solicitar. No\nse pone a extraer inglés de un resultado de herramienta. Así que el rechazo es legible para un\nhumano que lee logs e invisible para la maquinaria diseñada para manejar justo este caso.</p>\n<p>Y además llega tarde. El cuerpo de la herramienta corre después del enrutado, después de montar la\nsesión, después de que el servidor se ha comprometido a una respuesta con éxito. Para entonces ya\nno queda línea de estado que cambiar.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--el-desafío-que-la-especificación-realmente-quiere\">Paso 4 — El desafío que la especificación realmente quiere<a href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/#paso-4--el-desaf%C3%ADo-que-la-especificaci%C3%B3n-realmente-quiere\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — El desafío que la especificación realmente quiere\" title=\"Enlace directo al Paso 4 — El desafío que la especificación realmente quiere\" translate=\"no\">​</a></h2>\n<p>Conseguir un <code>403</code> significa comprobar los ámbitos <em>antes</em> de que responda la capa JSON-RPC, lo\nque significa un middleware ASGI envolviendo la app de FastMCP:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">ScopeChallengeMiddleware</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">__init__</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">self</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> app</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        self</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">app </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> app</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">__call__</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">self</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> scope</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> receive</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> send</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> scope</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">!=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> scope</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"method\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">!=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"POST\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> self</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">app</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">scope</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> receive</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> send</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Buffer the body so we can inspect it and still pass it on.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        chunks</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> more </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">while</span><span class=\"token plain\"> more</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            msg </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> receive</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            chunks</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">append</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">msg</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"body\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">b\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            more </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> msg</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"more_body\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">False</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        body </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">b\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">join</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">chunks</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        needed </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">try</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            rpc </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> json</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">loads</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">body</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> rpc</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"method\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">==</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"tools/call\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                needed </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> TOOL_SCOPES</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">rpc</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"params\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"name\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">except</span><span class=\"token plain\"> Exception</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">pass</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> needed </span><span class=\"token keyword\" style=\"color:#00009f\">and</span><span class=\"token plain\"> needed </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> token_scopes</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">scope</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"headers\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            challenge </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f'Bearer error=\"insufficient_scope\", scope=\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">needed</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\", '</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f'resource_metadata=\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">RESOURCE_METADATA</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\", '</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f'error_description=\"This operation requires the </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">needed</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> scope\"'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> send</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http.response.start\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"status\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">403</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                        </span><span class=\"token string\" style=\"color:#e3116c\">\"headers\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">b\"content-type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">b\"application/json\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                    </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">b\"www-authenticate\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> challenge</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">encode</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> send</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http.response.body\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                        </span><span class=\"token string\" style=\"color:#e3116c\">\"body\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> json</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">dumps</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"error\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"insufficient_scope\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                            </span><span class=\"token string\" style=\"color:#e3116c\">\"scope\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> needed</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">encode</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Replay the buffered body downstream.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        replayed </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">False</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">replay</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">nonlocal</span><span class=\"token plain\"> replayed</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> replayed</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                replayed </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http.request\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"body\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> body</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"more_body\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">False</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> receive</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> self</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">app</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">scope</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> replay</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> send</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">app </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ScopeChallengeMiddleware</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">mcp</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">http_app</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">stateless_http</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>La misma petición, contra <code>:8772</code>:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El middleware devuelve 403 con un desafío WWW-Authenticate que nombra el ámbito que falta\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h10-403-challenge-2a8d3ff52be96195ff042471605c0c4e.png\" width=\"827\" height=\"247\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">HTTP/1.1 403 Forbidden</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">www-authenticate: Bearer error=\"insufficient_scope\", scope=\"office:refund\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  resource_metadata=\"http://127.0.0.1:8772/.well-known/oauth-protected-resource\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  error_description=\"This operation requires the office:refund scope\"</span><br></div></code></pre></div></div>\n<p>Ahora el cliente tiene algo sobre lo que actuar: el estado dice rechazado, <code>scope=</code> dice qué\npedir, y <code>resource_metadata</code> dice dónde averiguar cómo. Eso es autorización escalada como\nprotocolo, no como mensaje de log.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5--lo-que-costó\">Paso 5 — Lo que costó<a href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/#paso-5--lo-que-cost%C3%B3\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 5 — Lo que costó\" title=\"Enlace directo al Paso 5 — Lo que costó\" translate=\"no\">​</a></h2>\n<p>El middleware funciona. También es peor código que el decorador, de tres formas concretas, y\nfingir lo contrario sería deshonesto.</p>\n<p><strong>No sabe qué es una herramienta.</strong> El middleware ASGI ve bytes y cabeceras. Para averiguar qué\nherramienta se está llamando, parsea él mismo el sobre JSON-RPC y busca el nombre en una tabla que\ntiene que mantener:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">TOOL_SCOPES </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"find_customer\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"office:read\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"open_invoices\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"office:read\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"issue_refund\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">  </span><span class=\"token string\" style=\"color:#e3116c\">\"office:refund\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>Esa tabla es una segunda fuente de verdad. Añade una herramienta y olvida la entrada y queda sin\nproteger — en silencio, porque el middleware simplemente deja pasar cualquier cosa que no\nreconoce. El decorador no podía tener ese fallo: el requisito estaba sobre la función.</p>\n<p><strong>Decodifica el token sin verificarlo.</strong> El middleware corre <em>antes</em> del verificador, así que los\nclaims verificados todavía no existen. Parte el JWT y decodifica en base64 el payload sin\ncomprobar la firma:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">payload </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> auth</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">split</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">split</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\".\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">claims </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> json</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">loads</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">base64</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">urlsafe_b64decode</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">payload </span><span class=\"token operator\" style=\"color:#393A34\">+</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"=\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">*</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token builtin\">len</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">payload</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">%</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">4</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Esto no es el agujero que parece — el verificador sigue corriendo después y sigue rechazando un\ntoken falsificado, así que nada llega a una herramienta con una firma mala. Pero la <em>decisión de\námbito</em> se toma sobre bytes no autenticados, y la única razón por la que eso es sobrevivible es la\nsegunda comprobación detrás. Es una verruga, no una vulnerabilidad, y es de esas cosas que vale la\npena dejar escritas antes de que alguien más tarde quite el verificador \"redundante\".</p>\n<p><strong>Almacena en memoria cada cuerpo de petición.</strong> Para leer el sobre y aun así pasarlo hacia abajo,\nel middleware vacía <code>receive()</code> en memoria y lo reproduce. Bien para llamadas JSON-RPC; piénsalo\nmejor antes de poner esto delante de subidas grandes.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"resolución-de-problemas--los-errores-que-esta-ejecución-produjo-de-verdad\">Resolución de problemas — los errores que esta ejecución produjo de verdad<a href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/#resoluci%C3%B3n-de-problemas--los-errores-que-esta-ejecuci%C3%B3n-produjo-de-verdad\" class=\"hash-link\" aria-label=\"Enlace directo al Resolución de problemas — los errores que esta ejecución produjo de verdad\" title=\"Enlace directo al Resolución de problemas — los errores que esta ejecución produjo de verdad\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"insufficient_scope-en-una-herramienta-que-sí-concediste\"><code>insufficient_scope</code> en una herramienta que sí concediste<a href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/#insufficient_scope-en-una-herramienta-que-s%C3%AD-concediste\" class=\"hash-link\" aria-label=\"Enlace directo al insufficient_scope-en-una-herramienta-que-sí-concediste\" title=\"Enlace directo al insufficient_scope-en-una-herramienta-que-sí-concediste\" translate=\"no\">​</a></h3>\n<p>Revisa el token, no el servidor:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/mcp-auth/token.sh </span><span class=\"token string\" style=\"color:#e3116c\">\"mcp:invoke office:refund\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">cut</span><span class=\"token plain\"> -d. </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-f2</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> base64 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq .scope</span><br></div></code></pre></div></div>\n<p>Si falta <code>office:refund</code>, el ámbito existe como Property Mapping pero nunca se movió a los\n<em>Selected Scopes</em> del proveedor. Authentik descarta en silencio los ámbitos que un cliente no\ntiene permitido solicitar en lugar de dar error, así que el token vuelve válido y corto.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-middleware-nunca-se-dispara\">El middleware nunca se dispara<a href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/#el-middleware-nunca-se-dispara\" class=\"hash-link\" aria-label=\"Enlace directo al El middleware nunca se dispara\" title=\"Enlace directo al El middleware nunca se dispara\" translate=\"no\">​</a></h3>\n<p>Solo inspecciona <code>POST</code>. Los clientes MCP abren primero un <code>GET</code> para el flujo de eventos, y esa\npetición no lleva sobre JSON-RPC — si estás mirando la petición equivocada concluirás que el\nmiddleware está muerto. Confírmalo con el <code>curl</code> de arriba, que es un único <code>POST</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"de-repente-todas-las-herramientas-devuelven-403\">De repente todas las herramientas devuelven 403<a href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/#de-repente-todas-las-herramientas-devuelven-403\" class=\"hash-link\" aria-label=\"Enlace directo al De repente todas las herramientas devuelven 403\" title=\"Enlace directo al De repente todas las herramientas devuelven 403\" translate=\"no\">​</a></h3>\n<p><code>TOOL_SCOPES</code> está indexado por el nombre registrado de la herramienta, que es el nombre de la\n<em>función</em>, no la etiqueta decorada. Renombra una función y la tabla deja de coincidir. El caso de\npaso libre es la dirección peligrosa (sin proteger), pero un error tipográfico en la tabla produce\neste otro.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-cuerpo-del-403-está-vacío-en-algunos-clientes\">El cuerpo del 403 está vacío en algunos clientes<a href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/#el-cuerpo-del-403-est%C3%A1-vac%C3%ADo-en-algunos-clientes\" class=\"hash-link\" aria-label=\"Enlace directo al El cuerpo del 403 está vacío en algunos clientes\" title=\"Enlace directo al El cuerpo del 403 está vacío en algunos clientes\" translate=\"no\">​</a></h3>\n<p>El desafío vive en la <strong>cabecera</strong> <code>WWW-Authenticate</code>. Los clientes que solo registran el cuerpo\nde la respuesta te mostrarán <code>{\"error\":\"insufficient_scope\"}</code> y nada sobre qué ámbito. Usa\n<code>curl -i</code>.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-te-dio-esto-y-qué-no\">Qué te dio esto y qué no<a href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/#qu%C3%A9-te-dio-esto-y-qu%C3%A9-no\" class=\"hash-link\" aria-label=\"Enlace directo al Qué te dio esto y qué no\" title=\"Enlace directo al Qué te dio esto y qué no\" translate=\"no\">​</a></h2>\n<p><strong>Te dio</strong> autorización por herramienta de verdad: dos tokens que se diferencian en un ámbito,\ncada uno capaz de ejecutar exactamente una de dos herramientas, demostrado en la capa HTTP en\nlugar de afirmado. Y con el middleware, un rechazo del que un cliente puede recuperarse de forma\nprogramática.</p>\n<p><strong>No te dio</strong> un diseño limpio. Ambas implementaciones son concesiones que apuntan en direcciones\nopuestas:</p>\n<table><thead><tr><th></th><th>decorador de herramienta (<code>:8771</code>)</th><th>middleware ASGI (<code>:8772</code>)</th></tr></thead><tbody><tr><td>El requisito de ámbito vive</td><td>en la función</td><td>en una tabla aparte</td></tr><tr><td>Lee claims verificados</td><td>sí</td><td>no — decodifica sin verificar, el verificador corre después</td></tr><tr><td>Rechazo</td><td>HTTP 200, <code>isError: true</code></td><td>HTTP 403 + <code>WWW-Authenticate</code></td></tr><tr><td>El cliente puede escalar</td><td>no</td><td>sí</td></tr><tr><td>Herramienta nueva sin proteger por defecto</td><td>no</td><td><strong>sí</strong></td></tr></tbody></table>\n<p>El resumen honesto es que el comportamiento correcto del protocolo requiere salir de la\nabstracción que te da el framework, y la versión ergonómica no puede producirlo. Si tus clientes\nno implementan escalado — y la mayoría de los clientes de agentes hoy no lo hacen — el decorador es\nel mejor trato. Si lo hacen, lo pagas con una tabla que tienes que acordarte de actualizar.</p>\n<p><strong>Tampoco te dio</strong> una autorización que sobreviva a que la herramienta haga otra cosa.\n<code>issue_refund</code> está protegido por <code>office:refund</code>; nada impide que un futuro <code>find_customer</code> se\nedite para escribir. Los ámbitos controlan la entrada, no el comportamiento — que es el mismo\nlímite que la <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/\">parte 4</a> trazó alrededor de la\npolítica-en-la-herramienta.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-sigue\">Qué sigue<a href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al Qué sigue\" title=\"Enlace directo al Qué sigue\" translate=\"no\">​</a></h2>\n<p>El hueco evidente que queda es que ambas versiones confían en la lista de ámbitos del token y en\nnada más. Ninguna pregunta <em>quién</em> es quien llama ni <em>sobre qué</em> está actuando — un token con\n<code>office:refund</code> reembolsa cualquier factura, de cualquier cliente, por cualquier importe. Eso es\nautorización a nivel de objeto, y no vive en los ámbitos de OAuth en absoluto.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>A scope per tool, and a refusal clients can act on</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"más-lectura\">Más lectura<a href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/#m%C3%A1s-lectura\" class=\"hash-link\" aria-label=\"Enlace directo al Más lectura\" title=\"Enlace directo al Más lectura\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://modelcontextprotocol.io/specification/basic/authorization\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">MCP specification — authorization</a></li>\n<li class=\"\"><a href=\"https://datatracker.ietf.org/doc/html/rfc6750#section-3\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">RFC 6750 §3 — the <code>WWW-Authenticate</code> challenge</a></li>\n<li class=\"\"><a href=\"https://datatracker.ietf.org/doc/html/rfc9728\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">RFC 9728 — OAuth 2.0 Protected Resource Metadata</a></li>\n<li class=\"\"><a href=\"https://docs.goauthentik.io/docs/providers/property-mappings/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Authentik — property mappings and scopes</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/",
            "title": "Dale a cada herramienta MCP su propio ámbito y devuelve un rechazo sobre el que el cliente pueda actuar",
            "summary": "La parte 6 protegió todo el servidor con un solo ámbito, lo que significaba que el token que dejaba a un agente consultar un cliente también le dejaba emitir reembolsos. Separarlo por herramienta es fácil. Hacer que el rechazo sea útil no lo es: la implementación obvia devuelve HTTP 200 con el error enterrado en el cuerpo, así que el cliente no tiene nada con lo que disparar una autorización escalada. Conseguir el 403 y el desafío WWW-Authenticate que define la especificación implica salir por completo de la abstracción del framework — y la versión que lo hace decodifica el token una segunda vez, sin verificar.",
            "date_modified": "2026-09-18T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "seguridad",
                "mcp",
                "oauth2",
                "authentik",
                "jwt",
                "ámbitos",
                "agentes",
                "autoalojamiento",
                "endurecimiento"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/",
            "content_html": "<div class=\"tutorialHero\"><svg viewBox=\"0 0 24 24\" fill=\"#2496ED\" aria-label=\"Docker\" class=\"tutorialHero__docker\"><path d=\"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z\"></path></svg><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Lock down Docker networks</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run containers as non-root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Membership, not just an account</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">An identity for the agent, not a key</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">A tool server that checks who is asking</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Where the agent can go, not just what it can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/\"><span class=\"skillTracker__dot\" data-state=\"locked\">8</span><span class=\"skillTracker__skill\" data-state=\"locked\">Move the daemon off root</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">9</span><span class=\"skillTracker__skill\" data-state=\"current\">Run the model's own code without trusting it</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">A scope per tool, and a refusal clients can act on</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>Todas las partes de esta serie hasta ahora le han dado al modelo <em>herramientas</em>: funciones que\nescribiste tú, con argumentos que definiste tú. Este post va sobre la otra cosa que hacen los\nagentes, que es escribir código y después ejecutarlo.</p>\n<p>Es un riesgo distinto, y vale la pena ser preciso sobre la diferencia. Una llamada a herramienta\nes el modelo eligiendo de un menú que tú controlas. Ejecutar código generado es el modelo\npasándote algo que nadie ha leído nunca, y que tú ejecutas en tu máquina.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>Una WEC Instance: 8 vCPU AMD EPYC 7601, 15 GB de RAM, Ubuntu 22.04, kernel 5.15, systemd 249.\nDocker rootless <strong>29.8.1</strong> de la <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/\">Parte 8</a>, corriendo\njunto al demonio del sistema. La imagen de prueba es <code>python:3.12-alpine</code>. El host mantuvo 20\ncontenedores en marcha durante todo el proceso.</p><p>Todas las cifras y mensajes de error de abajo provienen de esa ejecución.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"por-qué-esto-merece-su-propio-post\">Por qué esto merece su propio post<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/#por-qu%C3%A9-esto-merece-su-propio-post\" class=\"hash-link\" aria-label=\"Enlace directo al Por qué esto merece su propio post\" title=\"Enlace directo al Por qué esto merece su propio post\" translate=\"no\">​</a></h2>\n<p>La industria tiene un nombre para esto. El riesgo\n<a href=\"https://genai.owasp.org/llmrisk/llm052025-improper-output-handling/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">LLM05:2025 Improper Output Handling</a>\nde OWASP lo lista como ejemplo de vulnerabilidad, con estas palabras:</p>\n<blockquote>\n<p>LLM output is entered directly into a system shell or similar function such as exec or eval,\nresulting in remote code execution.</p>\n</blockquote>\n<p>Y su consejo sobre cómo pensar en el modelo es exactamente el correcto:</p>\n<blockquote>\n<p>Treat the model as any other user, adopting a zero-trust approach</p>\n</blockquote>\n<p>Pero aquí está lo que hace que esa página merezca una lectura atenta. Te dice que trates la\nsalida del modelo como no confiable, nombra la ejecución de código como la consecuencia, y\ndespués su sección de prevención habla únicamente de <em>validar texto</em>: codificación de salida,\nconsultas parametrizadas, políticas de seguridad de contenido, monitoreo.</p>\n<p>Busca en esa página <strong>sandbox</strong>, <strong>container</strong>, <strong>least privilege</strong> o <strong>isolation</strong> y obtienes\ncero resultados. No pocos. Ninguno.</p>\n<p>No es una crítica a OWASP; el riesgo trata sobre el manejo de la salida, y validar es la\nrespuesta correcta para una salida que se convierte en HTML o SQL. Pero cuando la salida es un\nprograma que estás a punto de ejecutar, validar deja de ser posible. No puedes averiguar con una\nexpresión regular si cien líneas de Python son seguras.</p>\n<p>Así que la guía nombra el problema y se detiene una capa por encima del arreglo. Este post es esa\ncapa.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-permite-la-versión-obvia\">Lo que permite la versión obvia<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/#lo-que-permite-la-versi%C3%B3n-obvia\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que permite la versión obvia\" title=\"Enlace directo al Lo que permite la versión obvia\" translate=\"no\">​</a></h2>\n<p>La implementación ingenua es la que casi todo el mundo despliega primero, y parece responsable\n— al fin y al cabo el código corre en un contenedor:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> python:3.12-alpine python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"&lt;el código del modelo&gt;\"</span><br></div></code></pre></div></div>\n<p>Preguntémosle a ese contenedor hasta dónde llega.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> python:3.12-alpine python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">import os, urllib.request</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('uid inside     :', os.getuid())</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('can see host / :', sorted(os.listdir('/'))[:10])</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">try:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    r = urllib.request.urlopen('https://api.github.com', timeout=5)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print('internet       : REACHED', r.status)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">except Exception as e:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print('internet       : blocked', type(e).__name__)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> python:3.12-alpine </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">echo \"memory limit : $(cat /sys/fs/cgroup/memory.max)\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">echo \"pids limit   : $(cat /sys/fs/cgroup/pids.max)\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">echo \"cpus visible : $(nproc)\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">echo \"writable fs  : $(touch /proc-test 2&gt;/dev/null &amp;&amp; echo yes || echo no)\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">'</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Un contenedor ejecutando código del modelo informa uid 0, acceso completo a internet devolviendo HTTP 200, memoria sin límite, un límite de pids de 19136, ocho CPUs visibles y un sistema de archivos escribible\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h9-naive-sandbox-06b5b3907235d86115ef158babda8df0.png\" width=\"781\" height=\"561\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">internet       : REACHED 200</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">memory limit : max</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">pids limit   : 19136</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">cpus visible : 8</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">writable fs  : yes</span><br></div></code></pre></div></div>\n<p>Léelo como la lista de lo que el código generado tiene permitido hacer en tu máquina:</p>\n<ul>\n<li class=\"\"><strong>Llegar a internet.</strong> Cualquier cosa que encuentre, puede enviarla a algún sitio.</li>\n<li class=\"\"><strong>Quedarse con toda la memoria.</strong> <code>max</code> significa sin límite. En este host son 15 GB, y el\nkernel empezará a matar otras cosas para proporcionarlos.</li>\n<li class=\"\"><strong>Arrancar 19.136 procesos.</strong> Suficiente para dejar el host inutilizable.</li>\n<li class=\"\"><strong>Usar los ocho núcleos.</strong></li>\n<li class=\"\"><strong>Escribir en cualquier parte del contenedor</strong>, incluido llenar el disco.</li>\n</ul>\n<p>La Parte 8 sacó al demonio de root, así que este código ya no puede quedarse con el <em>host</em>.\nAquello valía la pena y aquí no ayuda en absoluto. Ninguno de los cinco puntos de arriba requiere\nser root.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"cerrarlo\">Cerrarlo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/#cerrarlo\" class=\"hash-link\" aria-label=\"Enlace directo al Cerrarlo\" title=\"Enlace directo al Cerrarlo\" translate=\"no\">​</a></h2>\n<p>Docker tiene flags para cada uno de estos. No son vistosas y son el arreglo entero:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--network</span><span class=\"token plain\"> none </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--memory</span><span class=\"token plain\"> 256m --pids-limit </span><span class=\"token number\" style=\"color:#36acaa\">64</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--cpus</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0.5</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --read-only </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--tmpfs</span><span class=\"token plain\"> /tmp:size</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">16m </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --cap-drop</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">ALL --security-opt no-new-privileges </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  python:3.12-alpine </span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">comando</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><br></div></code></pre></div></div>\n<p>En palabras llanas:</p>\n<table><thead><tr><th>Flag</th><th>Qué impide</th></tr></thead><tbody><tr><td><code>--network none</code></td><td>Sin red en absoluto — no se puede enviar nada a ninguna parte</td></tr><tr><td><code>--memory 256m</code></td><td>Si usa más de 256 MB, el kernel lo mata a él, no a tus otros servicios</td></tr><tr><td><code>--pids-limit 64</code></td><td>Una bomba fork llega a 64 y se para</td></tr><tr><td><code>--cpus 0.5</code></td><td>Un bucle infinito se queda con medio núcleo</td></tr><tr><td><code>--read-only</code></td><td>No se puede escribir nada en el sistema de archivos del contenedor</td></tr><tr><td><code>--tmpfs /tmp:size=16m</code></td><td>Salvo un área pequeña de trabajo, limitada y en memoria</td></tr><tr><td><code>--cap-drop=ALL</code></td><td>Sin privilegios de kernel — nada de montar, ni sockets crudos, nada</td></tr><tr><td><code>--security-opt no-new-privileges</code></td><td>Nada de dentro puede ganar más privilegios de los que tenía</td></tr></tbody></table>\n<p>Ejecútalo, y falla antes incluso de arrancar el contenedor:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Docker rechaza la ejecución con el error NanoCPUs can not be set, as your kernel does not support CPU CFS scheduler or the cgroup is not mounted\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h9-cpus-refused-0a8c67fa115c08d21d53054814b86f30.png\" width=\"994\" height=\"344\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">docker: Error response from daemon: NanoCPUs can not be set, as your kernel does not</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">support CPU CFS scheduler or the cgroup is not mounted</span><br></div></code></pre></div></div>\n<p>Retén ese mensaje, porque induce a error. El kernel soporta límites de CPU perfectamente — este\nhost ejecuta otros 20 contenedores con ellos disponibles. Volveremos a qué está mal de verdad.</p>\n<p>Quita <code>--cpus</code> y todo lo demás se aplica:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El contenedor endurecido informa un límite de memoria de 256 MiB, un límite de pids de 64, un sistema de archivos de solo lectura, un tmp escribible, y acceso de red bloqueado con URLError\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h9-hardened-holds-e5ed1f26eadb61b1c097d1438eae1520.png\" width=\"751\" height=\"396\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">memory limit : 268435456        &lt;- exactamente 256 MiB</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">pids limit   : 64</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">cpus visible : 8</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">writable fs  : no</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">tmp writable : yes</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">internet     : blocked URLError</span><br></div></code></pre></div></div>\n<p>Cinco de seis controles funcionando. Y <code>cpus visible: 8</code>, porque no nos dejaron poner uno.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"demostrar-que-el-hueco-es-real\">Demostrar que el hueco es real<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/#demostrar-que-el-hueco-es-real\" class=\"hash-link\" aria-label=\"Enlace directo al Demostrar que el hueco es real\" title=\"Enlace directo al Demostrar que el hueco es real\" translate=\"no\">​</a></h2>\n<p>Un límite de CPU que falta suena leve al lado de \"memoria sin límite\". No lo es, porque un bucle\ninfinito es lo más probable que hace el código generado sin querer.</p>\n<p>Esto lanza ocho bucles ocupados dentro del sandbox por lo demás completamente cerrado, durante\nocho segundos:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--name</span><span class=\"token plain\"> burn </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--network</span><span class=\"token plain\"> none </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--memory</span><span class=\"token plain\"> 256m --pids-limit </span><span class=\"token number\" style=\"color:#36acaa\">64</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --read-only </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--tmpfs</span><span class=\"token plain\"> /tmp:size</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">16m </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --cap-drop</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">ALL --security-opt no-new-privileges </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  python:3.12-alpine </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'for i in $(seq 8); do (while :; do :; done) &amp; done; sleep 8'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">3</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless stats --no-stream burn</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker stats mostrando el contenedor burn consumiendo 605,19 por ciento de CPU mientras la memoria se queda en 1,23 MiB de 256 MiB, la red a cero y 9 procesos\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h9-burn-605-a829703726df3283acc0afa12d391696.png\" width=\"678\" height=\"208\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">NAME   CPU %     MEM USAGE / LIMIT   NET I/O   PIDS</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">burn   605.19%   1.23MiB / 256MiB    0B / 0B   9</span><br></div></code></pre></div></div>\n<p><strong>605% de una CPU.</strong> Seis de los ocho núcleos del host, tomados por código dentro de un sandbox\ndonde la memoria está limitada a 256 MB (usó 1,23), los procesos a 64 (usó 9) y la red está\ncompletamente bloqueada.</p>\n<p>Todos los controles que pusimos funcionan. El único que no pudimos poner es el que se está\nabusando.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-está-mal-de-verdad\">Qué está mal de verdad<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/#qu%C3%A9-est%C3%A1-mal-de-verdad\" class=\"hash-link\" aria-label=\"Enlace directo al Qué está mal de verdad\" title=\"Enlace directo al Qué está mal de verdad\" translate=\"no\">​</a></h2>\n<p>El mensaje de error culpaba al kernel. El kernel está bien. La respuesta real es un archivo.</p>\n<p>Cuando Docker corre en modo rootless no gestiona recursos directamente: solo puede usar lo que el\nsistema le haya entregado a tu cuenta de usuario. En Linux moderno esa entrega se llama\n<strong>delegación de cgroups</strong>, y quien decide qué se delega es systemd.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> /sys/fs/cgroup/user.slice/user-1000.slice/cgroup.controllers</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">memory pids</span><br></div></code></pre></div></div>\n<p>Dos cosas. Esa es la explicación entera:</p>\n<!-- -->\n<p>El demonio lo sabía desde el principio. Lo dijo al arrancar, diez veces, en líneas que nadie lee:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">journalctl </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--user</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-u</span><span class=\"token plain\"> docker.service </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"no .* support\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El log de arranque del demonio rootless listando diez advertencias entre ellas no cpu cfs quota support, no cpuset support, no io.weight support y no io.max support\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h9-delegation-root-cause-801bd67d214cce22c58da71540cddebb.png\" width=\"1233\" height=\"260\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING: No cpu cfs quota support</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING: No cpu cfs period support</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING: No cpu shares support</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING: No cpuset support</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING: No io.weight support</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING: No io.max (rbps) support</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">...</span><br></div></code></pre></div></div>\n<p>Esto también explica algo que la <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/\">Parte 8</a> reportó como\nuna limitación sin más del modo rootless: que los límites de rendimiento de disco no funcionan.\nMisma causa. No es una propiedad de rootless, sino de lo que systemd delegó.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"dónde-está-documentado\">Dónde está documentado<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/#d%C3%B3nde-est%C3%A1-documentado\" class=\"hash-link\" aria-label=\"Enlace directo al Dónde está documentado\" title=\"Enlace directo al Dónde está documentado\" translate=\"no\">​</a></h3>\n<p>No en la documentación rootless de Docker. Busqué en la página: <strong>cero</strong> apariciones de\n\"delegate\". La guía está río arriba, en el\n<a href=\"https://rootlesscontaine.rs/getting-started/common/cgroup2/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">sitio de rootless containers</a>, que\nlo dice directamente:</p>\n<blockquote>\n<p>By default, a non-root user can only get memory controller and pids controller to be delegated.</p>\n</blockquote>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-arreglo\">El arreglo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/#el-arreglo\" class=\"hash-link\" aria-label=\"Enlace directo al El arreglo\" title=\"Enlace directo al El arreglo\" translate=\"no\">​</a></h2>\n<p>Un archivo drop-in que le dice a systemd que entregue más:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">mkdir</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> /etc/systemd/system/user@.service.d</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token string bash punctuation\" style=\"color:#393A34\"> </span><span class=\"token string bash punctuation operator\" style=\"color:#393A34\">|</span><span class=\"token string bash punctuation\" style=\"color:#393A34\"> </span><span class=\"token string bash punctuation function\" style=\"color:#d73a49\">sudo</span><span class=\"token string bash punctuation\" style=\"color:#393A34\"> </span><span class=\"token string bash punctuation function\" style=\"color:#d73a49\">tee</span><span class=\"token string bash punctuation\" style=\"color:#393A34\"> /etc/systemd/system/user@.service.d/delegate.conf</span><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">[Service]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">Delegate=cpu cpuset io memory pids</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> systemctl daemon-reload</span><br></div></code></pre></div></div>\n<p>La misma página advierte:</p>\n<blockquote>\n<p>After changing the systemd configuration, you need to re-login or reboot the host. Rebooting\nthe host is recommended.</p>\n</blockquote>\n<p>Con systemd 249 resultó no hacer falta. Solo con <code>daemon-reload</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\">cat</span><span class=\"token plain\"> /sys/fs/cgroup/user.slice/user-1000.slice/cgroup.controllers</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">cpuset cpu io memory pids</span><br></div></code></pre></div></div>\n<p>El demonio sí hay que reiniciarlo, porque lee sus capacidades una sola vez al arrancar. Reinicia\n<strong>solo el demonio rootless</strong>, no toda tu sesión de usuario, que en este host habría tumbado\nademás un servicio sin relación:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">systemctl </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--user</span><span class=\"token plain\"> restart docker.service</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">journalctl </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--user</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-u</span><span class=\"token plain\"> docker.service </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--since</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"-1min\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-ci</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"no cpu\\|no io\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El archivo delegate.conf siendo escrito, seguido de un reinicio del servicio docker rootless y un recuento de advertencias de cero\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h9-delegate-fix-1f7414c6361fa1fc13be57138940d1f5.png\" width=\"646\" height=\"206\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">0</span><br></div></code></pre></div></div>\n<p>Cero advertencias, donde había diez.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"los-mismos-ocho-bucles-otra-vez\">Los mismos ocho bucles, otra vez<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/#los-mismos-ocho-bucles-otra-vez\" class=\"hash-link\" aria-label=\"Enlace directo al Los mismos ocho bucles, otra vez\" title=\"Enlace directo al Los mismos ocho bucles, otra vez\" translate=\"no\">​</a></h2>\n<p>Comando idéntico al anterior. Una flag añadida:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--name</span><span class=\"token plain\"> burn2 </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--network</span><span class=\"token plain\"> none </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--memory</span><span class=\"token plain\"> 256m --pids-limit </span><span class=\"token number\" style=\"color:#36acaa\">64</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--cpus</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0.5</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --read-only </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--tmpfs</span><span class=\"token plain\"> /tmp:size</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">16m </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --cap-drop</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">ALL --security-opt no-new-privileges </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  python:3.12-alpine </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'for i in $(seq 8); do (while :; do :; done) &amp; done; sleep 8'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">3</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless stats --no-stream burn2</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker stats mostrando el contenedor burn2 contenido en 52,82 por ciento de CPU con las mismas cifras de memoria, red y procesos que antes\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h9-burn-capped-7dff3d60a38ee6b89cb8667301faea0f.png\" width=\"665\" height=\"204\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">NAME    CPU %    MEM USAGE / LIMIT    NET I/O   PIDS</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">burn2   52.82%   1.242MiB / 256MiB    0B / 0B   9</span><br></div></code></pre></div></div>\n<p><strong>605,19% → 52,82%.</strong> Los mismos ocho bucles infinitos, la misma imagen, todo igual. Medio\nnúcleo, como se pidió.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-deberías-llevarte-de-esto\">Qué deberías llevarte de esto<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/#qu%C3%A9-deber%C3%ADas-llevarte-de-esto\" class=\"hash-link\" aria-label=\"Enlace directo al Qué deberías llevarte de esto\" title=\"Enlace directo al Qué deberías llevarte de esto\" translate=\"no\">​</a></h2>\n<p><strong>Un contenedor no es un sandbox hasta que dices qué puede usar.</strong> El valor por defecto es\ngeneroso en todas las dimensiones que importan, y nada te avisa, porque un contenedor sin\nrestricciones es algo completamente normal de ejecutar.</p>\n<p><strong>Comprueba que tus límites se aplican, en lugar de darlo por hecho.</strong> Todo este post existe\nporque una flag que estaba silenciosamente no disponible se veía idéntica a una flag que\nfuncionaba. <code>docker stats</code> bajo una carga deliberada tarda un minuto y te dice la verdad.</p>\n<p><strong>Los mensajes de error apuntan a la capa equivocada.</strong> \"your kernel does not support CPU CFS\nscheduler\" describía un kernel que lo soporta perfectamente. La causa real era un valor por\ndefecto de systemd, dos capas más allá, documentado en otra web.</p>\n<p>Y el límite honesto: este sandbox contiene <em>recursos</em>. No vuelve seguro el código. No sabe\ndistinguir un script útil de uno hostil, y un programa que haga algo dañino dentro de 256 MB,\nmedio núcleo y sin red se ejecutará exactamente como está escrito. La contención te compra un\nradio de daño, no un criterio.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Run the model's own code without trusting it</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"más-lectura\">Más lectura<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/#m%C3%A1s-lectura\" class=\"hash-link\" aria-label=\"Enlace directo al Más lectura\" title=\"Enlace directo al Más lectura\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://genai.owasp.org/llmrisk/llm052025-improper-output-handling/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">OWASP — LLM05:2025 Improper Output Handling</a></li>\n<li class=\"\"><a href=\"https://docs.docker.com/engine/containers/resource_constraints/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker — Runtime options with Memory, CPUs, and GPUs</a></li>\n<li class=\"\"><a href=\"https://rootlesscontaine.rs/getting-started/common/cgroup2/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Rootless Containers — cgroup v2 delegation</a></li>\n<li class=\"\"><a href=\"https://docs.kernel.org/admin-guide/cgroup-v2.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Linux kernel — Control Group v2</a></li>\n<li class=\"\"><a href=\"https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><code>systemd.resource-control(5)</code> — <code>Delegate=</code></a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/",
            "title": "Aísla el código que escribe tu agente y comprueba que cada límite se aplicó de verdad",
            "summary": "OWASP nombra la ejecución de código generado por el modelo mediante exec o eval como una vulnerabilidad propia, te dice que trates al modelo como no confiable — y no dice nada sobre cómo contenerlo. Así que construimos la contención: sin red, 256 MB, 64 procesos, disco de solo lectura, sin capacidades. Todo eso funcionó. El límite de CPU fue rechazado de plano, y ocho bucles infinitos se llevaron el 605% del host. La causa es una línea de configuración de systemd que nadie menciona, y el arreglo no necesitó reiniciar pese a que la documentación insiste en lo contrario.",
            "date_modified": "2026-09-17T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "seguridad",
                "docker",
                "sandbox",
                "agentes",
                "ejecución-de-código",
                "cgroups",
                "autoalojamiento",
                "endurecimiento"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/",
            "content_html": "<div class=\"tutorialHero\"><svg viewBox=\"0 0 24 24\" fill=\"#2496ED\" aria-label=\"Docker\" class=\"tutorialHero__docker\"><path d=\"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z\"></path></svg><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Lock down Docker networks</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run containers as non-root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Membership, not just an account</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">An identity for the agent, not a key</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">A tool server that checks who is asking</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Where the agent can go, not just what it can call</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">8</span><span class=\"skillTracker__skill\" data-state=\"current\">Move the daemon off root</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/\"><span class=\"skillTracker__dot\" data-state=\"locked\">9</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run the model's own code without trusting it</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">A scope per tool, and a refusal clients can act on</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>Esta es la parte que nadie dice en voz alta cuando te recomiendan añadirte al grupo <code>docker</code>\npara dejar de escribir <code>sudo</code>.</p>\n<p>Ese grupo es root. No \"casi root\", ni \"root para cosas de Docker\". Si puedes ejecutar un\ncontenedor, puedes leer, modificar o borrar cualquier archivo de la máquina — incluido el\narchivo de contraseñas, los directorios personales de otras personas y los archivos que el\nadministrador apartó deliberadamente de ti.</p>\n<p>No hace falta ningún exploit. Es un comando, y tarda unos cuatro segundos.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>Una WEC Instance: 8 vCPU AMD EPYC 7601, 15 GB de RAM, Ubuntu 22.04, kernel 5.15. El demonio del\nsistema es el <code>docker.io</code> de Ubuntu <strong>29.1.3</strong>; el demonio rootless instalado aquí es <strong>29.8.1</strong>\ncon <code>rootlesskit</code> 3.1.0. La imagen de prueba es <code>alpine:3.20</code>. El host mantuvo 20 contenedores\nen marcha bajo el demonio del sistema durante todo el proceso, y ninguno se detuvo.</p><p>Todas las salidas y tiempos de abajo provienen de esa ejecución.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"demostrarlo-en-tu-propia-máquina\">Demostrarlo en tu propia máquina<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/#demostrarlo-en-tu-propia-m%C3%A1quina\" class=\"hash-link\" aria-label=\"Enlace directo al Demostrarlo en tu propia máquina\" title=\"Enlace directo al Demostrarlo en tu propia máquina\" translate=\"no\">​</a></h2>\n<p>Ejecuta esto como usuario normal. No hay ningún <code>sudo</code> dentro.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-v</span><span class=\"token plain\"> /:/host alpine:3.20 </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'touch /host/root/rootless-demo &amp;&amp; echo WROTE as uid $(id -u)'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">ls</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-l</span><span class=\"token plain\"> /root/rootless-demo</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Un usuario sin privilegios ejecuta un contenedor que crea un archivo dentro de /root, y el listado del host confirma que el archivo pertenece a root\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h8-docker-group-is-root-8007f44ffccf255e2de1e18d9bf1ca00.png\" width=\"1158\" height=\"178\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WROTE as uid 0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-rw-r--r-- 1 root root 0 Sep 16 20:58 /root/rootless-demo</span><br></div></code></pre></div></div>\n<p>Un usuario que ni siquiera puede listar <code>/root</code> acaba de crear un archivo dentro, propiedad de\nroot.</p>\n<p>Nada falló. Eso es el diseño. El demonio corre como root, le pediste montar el sistema de\narchivos completo, y lo hizo — porque un proceso root tiene permiso para ello. El \"root\" del\ncontenedor y el root del host son la misma cosa.</p>\n<p>Docker lo documenta con claridad, y conviene leer sus palabras antes que las mías. De\n<a href=\"https://docs.docker.com/engine/security/#docker-daemon-attack-surface\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker daemon attack surface</a>:</p>\n<blockquote>\n<p>only trusted users should be allowed to control your Docker daemon</p>\n</blockquote>\n<p>y, en la misma página:</p>\n<blockquote>\n<p>Docker allows you to share a directory between the Docker host and a guest container; and it\nallows you to do so without limiting the access rights of the container.</p>\n</blockquote>\n<p>Eso no es una advertencia sobre un fallo. Es la descripción de la función que acabamos de usar.</p>\n<p>Límpialo antes de seguir:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-f</span><span class=\"token plain\"> /root/rootless-demo</span><br></div></code></pre></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"por-qué-esto-es-peor-en-un-host-que-ejecuta-agentes\">Por qué esto es peor en un host que ejecuta agentes<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/#por-qu%C3%A9-esto-es-peor-en-un-host-que-ejecuta-agentes\" class=\"hash-link\" aria-label=\"Enlace directo al Por qué esto es peor en un host que ejecuta agentes\" title=\"Enlace directo al Por qué esto es peor en un host que ejecuta agentes\" translate=\"no\">​</a></h3>\n<p>Un agente de IA que ejecuta código que él mismo generó es, en términos de seguridad, ejecución\nremota de código que instalaste a propósito. No es una crítica a los agentes — es para lo que\nsirven.</p>\n<p>Así que la pregunta deja de ser <em>\"¿podría un atacante ejecutar código aquí?\"</em> — algo ya lo hace,\nconstantemente — y pasa a ser <em>\"¿hasta dónde llega ese código?\"</em>.</p>\n<p>Si llega al socket de Docker, la respuesta es \"a todo\", mediante el comando de arriba. Tus\nclaves de API. Tus claves SSH. El volumen de la base de datos. Todo, por muy cuidadosamente\nque hayas configurado el contenedor del propio agente.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-arregló-la-parte-2-y-qué-no\">Qué arregló la Parte 2 y qué no<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/#qu%C3%A9-arregl%C3%B3-la-parte-2-y-qu%C3%A9-no\" class=\"hash-link\" aria-label=\"Enlace directo al Qué arregló la Parte 2 y qué no\" title=\"Enlace directo al Qué arregló la Parte 2 y qué no\" translate=\"no\">​</a></h3>\n<p>La <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/\">Parte 2</a> encontró dos de seis contenedores\nde Langfuse corriendo como root y los arregló con <code>user: \"999:999\"</code>. Aquello valía la pena y\nsigue valiéndola.</p>\n<p>Pero cambia quién es el proceso <strong>dentro</strong> del contenedor. El demonio que lo arranca sigue\nsiendo root, y el socket sigue siendo una puerta a toda la máquina. La Parte 2 endureció al\npasajero. Este post va a por el conductor.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-hace-realmente-el-modo-rootless\">Qué hace realmente el modo rootless<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/#qu%C3%A9-hace-realmente-el-modo-rootless\" class=\"hash-link\" aria-label=\"Enlace directo al Qué hace realmente el modo rootless\" title=\"Enlace directo al Qué hace realmente el modo rootless\" translate=\"no\">​</a></h2>\n<p>El demonio deja de correr como root y corre como tú.</p>\n<p>Los contenedores siguen creyendo que tienen root — el software de dentro sigue funcionando con\nnormalidad. Pero ese \"root\" ahora lo traduce el kernel a una cuenta ordinaria y sin privilegios\nque no posee nada importante. Si algo se escapa del contenedor, no es el administrador. Es\n<code>ubuntu</code>.</p>\n<p>La traducción usa un bloque de IDs de usuario que el sistema reservó para ti. Aquí está la idea\ncompleta en una imagen — el mismo contenedor, bajo cada demonio:</p>\n<!-- -->\n<p>La mitad izquierda es por qué funcionó el primer comando. La derecha es lo que vamos a montar, y\nsu aritmética es algo que puedes verificar tú en un minuto.</p>\n<p>Esta traducción es una función del kernel de Linux llamada\n<a href=\"https://man7.org/linux/man-pages/man7/user_namespaces.7.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">espacio de nombres de usuario</a>,\nno un invento de Docker — Docker solo se lo pide al kernel. El bloque de IDs al que mapea se\ndeclara en <a href=\"https://man7.org/linux/man-pages/man5/subuid.5.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><code>/etc/subuid</code></a>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'^ubuntu:'</span><span class=\"token plain\"> /etc/subuid /etc/subgid</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">/etc/subuid:ubuntu:100000:65536</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">/etc/subgid:ubuntu:100000:65536</span><br></div></code></pre></div></div>\n<p>65.536 IDs a partir de 100000, solo tuyos. Retén ese número — el mapeo se convierte en\naritmética comprobable dentro de un momento.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"instalarlo-cuatro-muros\">Instalarlo: cuatro muros<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/#instalarlo-cuatro-muros\" class=\"hash-link\" aria-label=\"Enlace directo al Instalarlo: cuatro muros\" title=\"Enlace directo al Instalarlo: cuatro muros\" translate=\"no\">​</a></h2>\n<p>La documentación asume una instalación que esta máquina no tiene. Aquí está cada muro en el\norden en que te lo encuentras.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"muro-1--el-paquete-no-existe\">Muro 1 — el paquete no existe<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/#muro-1--el-paquete-no-existe\" class=\"hash-link\" aria-label=\"Enlace directo al Muro 1 — el paquete no existe\" title=\"Enlace directo al Muro 1 — el paquete no existe\" translate=\"no\">​</a></h3>\n<p>Todas las guías empiezan con la misma línea. Con el Docker de Ubuntu:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">apt-get</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">install</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> docker-ce-rootless-extras</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">dpkg </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-L</span><span class=\"token plain\"> docker.io </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-iE</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'rootless|rootlesskit'</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"apt informa de que no puede localizar el paquete docker-ce-rootless-extras, y listar el contenido del paquete docker.io buscando archivos rootless no devuelve nada\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h8-no-rootless-package-4540e2d1cdbee668a23f37364afdc535.png\" width=\"1190\" height=\"246\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">E: Unable to locate package docker-ce-rootless-extras</span><br></div></code></pre></div></div>\n<p>Ese paquete pertenece al repositorio propio de Docker. Este host usa el <code>docker.io</code> de Ubuntu, y\nel segundo comando no devuelve <strong>absolutamente nada</strong>: el paquete de la distribución no incluye\nninguna herramienta rootless.</p>\n<p>Eso importa porque te obliga a elegir. Puedes sustituir <code>docker.io</code> por Docker CE, lo que\nsignifica cambiar el demonio que ahora mismo ejecuta tus contenedores. O puedes instalar un\nsegundo demonio, propiedad de tu usuario, junto al primero. Con 20 servicios corriendo, la\nsegunda opción es la única sensata, y es la que sigue el resto de este post.</p>\n<p>Lo que sí necesitas de apt son los ayudantes de mapeo de IDs:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">apt-get</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">install</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> uidmap</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Si apt te ofrece reiniciar servicios</div><div class=\"admonitionContent_BuS1\"><p>En Ubuntu 22.04 esto puede lanzar un diálogo \"Daemons using outdated libraries\" con\n<code>containerd.service</code> y tu base de datos ya marcados. <code>uidmap</code> no necesita que se reinicie\nninguno. Desmárcalos, o cancela el diálogo — la instalación se completa igual.</p></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"muro-2--el-instalador-se-niega-a-ejecutarse\">Muro 2 — el instalador se niega a ejecutarse<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/#muro-2--el-instalador-se-niega-a-ejecutarse\" class=\"hash-link\" aria-label=\"Enlace directo al Muro 2 — el instalador se niega a ejecutarse\" title=\"Enlace directo al Muro 2 — el instalador se niega a ejecutarse\" translate=\"no\">​</a></h3>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-fsSL</span><span class=\"token plain\"> https://get.docker.com/rootless </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"># Installing stable version 29.8.1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Aborting because rootful Docker is running and accessible.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Set FORCE_ROOTLESS_INSTALL=1 to ignore.</span><br></div></code></pre></div></div>\n<p>La protección asume que estás <em>migrando</em> — que un demonio root en marcha significa que estás a\npunto de liarla. Ejecutar ambos a la vez a propósito no se menciona en ningún sitio, y la forma\nde continuar aparece solo dentro del texto del error.</p>\n<p>Fíjate en la versión que nombra: <strong>29.8.1</strong>, mientras que el demonio del sistema es <strong>29.1.3</strong>.\nVas a acabar con dos versiones distintas de Docker en un mismo host. Aquí es lo esperado, y\nconviene saberlo.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-fsSL</span><span class=\"token plain\"> https://get.docker.com/rootless </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">FORCE_ROOTLESS_INSTALL</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><br></div></code></pre></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"muro-3--te-mueve-la-cli-sin-avisarte\">Muro 3 — te mueve la CLI sin avisarte<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/#muro-3--te-mueve-la-cli-sin-avisarte\" class=\"hash-link\" aria-label=\"Enlace directo al Muro 3 — te mueve la CLI sin avisarte\" title=\"Enlace directo al Muro 3 — te mueve la CLI sin avisarte\" translate=\"no\">​</a></h3>\n<p>Lee las últimas líneas de la salida del instalador:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">[INFO] Creating CLI context \"rootless\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">[INFO] Using CLI context \"rootless\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Current context is now \"rootless\"</span><br></div></code></pre></div></div>\n<p>Ahora pregúntale a Docker qué está corriendo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> context </span><span class=\"token function\" style=\"color:#d73a49\">ls</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> default </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-q</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">wc</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-l</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker ps no lista ningún contenedor, docker context ls muestra el asterisco en rootless, y consultar el contexto default informa de 20 contenedores en marcha\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h8-context-hijack-4d422c845ec7d88ee2b2f5a5ffbed4af.png\" width=\"1182\" height=\"330\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">NAME         DESCRIPTION                              DOCKER ENDPOINT                     ERROR</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">default      Current DOCKER_HOST based configuration  unix:///var/run/docker.sock</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">rootless *   Rootless mode                            unix:///run/user/1000/docker.sock</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">20</span><br></div></code></pre></div></div>\n<p>Una lista vacía, mientras veinte contenedores funcionan perfectamente. Nada se rompió y nada te\navisó: la CLI está respondiendo a una pregunta distinta de la que crees haber hecho, porque el\ninstalador la reapuntó al terminar.</p>\n<p>Devuélvela a su sitio y decide por comando:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> context use default          </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># trabajo diario, sin cambios</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><span class=\"token plain\">               </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># explícito cuando quieras el nuevo</span><br></div></code></pre></div></div>\n<p>Arranca el demonio y confirma lo que tienes:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable environment constant\" style=\"color:#36acaa\">XDG_RUNTIME_DIR</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">/run/user/</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">id</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-u</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">systemctl </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--user</span><span class=\"token plain\"> start docker.service</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless info </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--format</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{{.ServerVersion}} {{.SecurityOptions}}'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">29.8.1 [name=seccomp,profile=builtin name=rootless name=cgroupns]</span><br></div></code></pre></div></div>\n<p><code>name=rootless</code> es el demonio confirmando lo que es.</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>El consejo sobre el PATH solo hace falta a medias</div><div class=\"admonitionContent_BuS1\"><p>El instalador te dice que añadas <code>~/bin</code> a tu <code>PATH</code>. Para el uso diario no hace falta: el\ncliente <code>docker</code> del sistema habla perfectamente con el demonio rootless a través del contexto,\ny todos los comandos de este post se ejecutaron así. El PATH solo lo necesitan los scripts de\ninstalación y desinstalación — y sin él fallan con\n<code>dockerd-rootless.sh needs to be present under $PATH</code>, que no te dice nada sobre el problema\nreal.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"comprobar-que-el-mapeo-es-real\">Comprobar que el mapeo es real<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/#comprobar-que-el-mapeo-es-real\" class=\"hash-link\" aria-label=\"Enlace directo al Comprobar que el mapeo es real\" title=\"Enlace directo al Comprobar que el mapeo es real\" translate=\"no\">​</a></h2>\n<p>Pregúntale a un contenedor quién es, y luego mira ese mismo proceso desde fuera:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> alpine:3.20 </span><span class=\"token function\" style=\"color:#d73a49\">id</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--name</span><span class=\"token plain\"> u0 alpine:3.20 </span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">60</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">ps</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">user</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">,uid</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">,cmd</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-C</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'sleep 60'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--name</span><span class=\"token plain\"> u1000 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--user</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1000</span><span class=\"token plain\">:1000 alpine:3.20 </span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">120</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">ps</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">uid</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">,cmd</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-C</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'sleep 120'</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El contenedor informa de uid 0 root; el host muestra ese proceso como propiedad de ubuntu uid 1000; un contenedor corriendo como uid 1000 aparece en el host como uid 100999\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h8-uid-mapping-d53136aff71ace37ef98f708404f45d5.png\" width=\"1186\" height=\"648\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">uid=0(root) gid=0(root) groups=0(root),...</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ubuntu    1000 sleep 60</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">100999 sleep 120</span><br></div></code></pre></div></div>\n<p>Lee esas tres líneas juntas, porque son el mecanismo entero:</p>\n<ul>\n<li class=\"\">El contenedor cree que es root.</li>\n<li class=\"\">El host dice que ese proceso pertenece a <code>ubuntu</code>.</li>\n<li class=\"\">Un contenedor que corre como ID 1000 aparece en el host como <strong>100999</strong>.</li>\n</ul>\n<p>Ese último número es <code>100000 + 1000 - 1</code> — el inicio de tu bloque reservado, desplazado por el\nID del propio contenedor, porque el root del contenedor ya ocupó la primera posición al mapearse\na ti.</p>\n<p>Lo que te da una prueba en lugar de una promesa. Calcúlalo desde tu propia línea de\n<code>/etc/subuid</code> y comprueba si el número coincide. <strong>Si en cambio ves un <code>1000</code> pelado, la\ntraducción no está ocurriendo</strong> y algo va mal en la instalación.</p>\n<p>Limpieza:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless </span><span class=\"token function\" style=\"color:#d73a49\">rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-f</span><span class=\"token plain\"> u0 u1000</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-mismo-comando-el-otro-demonio\">El mismo comando, el otro demonio<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/#el-mismo-comando-el-otro-demonio\" class=\"hash-link\" aria-label=\"Enlace directo al El mismo comando, el otro demonio\" title=\"Enlace directo al El mismo comando, el otro demonio\" translate=\"no\">​</a></h2>\n<p>Volvamos al comando con el que abrió este post. Misma imagen, mismo montaje del sistema de\narchivos completo, mismos flags. Solo cambia el demonio:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-v</span><span class=\"token plain\"> /:/host alpine:3.20 </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'touch /host/root/rootless-demo || echo DENIED'</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El comando idéntico que antes escribía dentro de /root ahora falla con permiso denegado bajo el demonio rootless\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h8-denied-e38f44dfdd6d58bf3f217438f3a584a0.png\" width=\"1186\" height=\"372\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">touch: /host/root/rootless-demo: Permission denied</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">DENIED</span><br></div></code></pre></div></div>\n<p>El contenedor sigue creyendo que es root. El kernel no está de acuerdo, porque el proceso que\nhay detrás pertenece a <code>ubuntu</code>, y <code>ubuntu</code> no puede escribir en <code>/root</code>.</p>\n<p>Ese es el post, en dos salidas.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-te-cuesta\">Lo que te cuesta<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/#lo-que-te-cuesta\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que te cuesta\" title=\"Enlace directo al Lo que te cuesta\" translate=\"no\">​</a></h2>\n<p>Cuatro cosas, todas medidas aquí en lugar de citadas.</p>\n<p><strong>Los contenedores no arrancan notablemente más lento.</strong> Tres ejecuciones seguidas:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> </span><span class=\"token for-or-select variable\" style=\"color:#36acaa\">i</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">3</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"> /usr/bin/time </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-f</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"run+exit: %es\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> alpine:3.20 </span><span class=\"token boolean\" style=\"color:#36acaa\">true</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">done</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">80</span><span class=\"token plain\">:80 alpine:3.20 </span><span class=\"token boolean\" style=\"color:#36acaa\">true</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Tres ejecuciones de contenedor completadas en 1,19, 0,96 y 0,97 segundos, seguidas del error de puerto privilegiado que rechaza exponer el puerto 80\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h8-costs-03291fa4bb34544ae9eff02c34a5fa7a.png\" width=\"1190\" height=\"546\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">run+exit: 1.19s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">run+exit: 0.96s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">run+exit: 0.97s</span><br></div></code></pre></div></div>\n<p><strong>Los puertos por debajo de 1024 se rechazan</strong>, y, cosa poco habitual, el error te da todas las\nsalidas:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">cannot expose privileged port 80, you can add</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">or set CAP_NET_BIND_SERVICE on rootlesskit binary,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">or choose a larger port number (&gt;= 1024)</span><br></div></code></pre></div></div>\n<p><code>-p 18080:80</code> se enlaza sin queja. Detrás de un proxy inverso esto rara vez importa.</p>\n<p><strong>Los límites de rendimiento de disco dejan de funcionar.</strong> El demonio lo dice al arrancar, y es\nfácil pasarlo por alto:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING: No io.max (rbps) support</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING: No io.max (wbps) support</span><br></div></code></pre></div></div>\n<p>Así que <code>--device-read-bps</code> y <code>--device-write-bps</code> no hacen nada, en silencio. Si contabas con\nellos para evitar que un contenedor ahogue a los demás, rootless te lo quita.</p>\n<p><strong>Descartar capacidades sigue funcionando</strong>, lo que importa porque a menudo se tratan como\nalternativas:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> --cap-drop</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">ALL alpine:3.20 </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'echo cap-drop ok'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">cap-drop ok</span><br></div></code></pre></div></div>\n<p>Se acumulan. Sigue usando <code>user:</code> y <code>--cap-drop=ALL</code> — cada capa asume que la anterior falló.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"las-carpetas-compartidas-se-comportan-distinto\">Las carpetas compartidas se comportan distinto<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/#las-carpetas-compartidas-se-comportan-distinto\" class=\"hash-link\" aria-label=\"Enlace directo al Las carpetas compartidas se comportan distinto\" title=\"Enlace directo al Las carpetas compartidas se comportan distinto\" translate=\"no\">​</a></h2>\n<p>Esta es la parte que pilla a quien mueve un servicio real. Los permisos del host se aplican como\n<em>tú</em>, no como root:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">D</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">/tmp/rl-vol</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">mkdir</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">$D</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> hello </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">$D</span><span class=\"token plain\">/mine.txt</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"echo secret &gt; </span><span class=\"token string variable\" style=\"color:#36acaa\">$D</span><span class=\"token string\" style=\"color:#e3116c\">/theirs.txt; chmod 600 </span><span class=\"token string variable\" style=\"color:#36acaa\">$D</span><span class=\"token string\" style=\"color:#e3116c\">/theirs.txt\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-v</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">$D</span><span class=\"token plain\">:/data alpine:3.20 </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token string\" style=\"color:#e3116c\">'cat /data/mine.txt; cat /data/theirs.txt 2&gt;&amp;1 || echo DENIED'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-v</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">$D</span><span class=\"token plain\">:/data alpine:3.20 </span><span class=\"token function\" style=\"color:#d73a49\">stat</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'%u:%g %n'</span><span class=\"token plain\"> /data/mine.txt</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El contenedor lee el archivo del usuario, se le deniega el archivo de root, e informa del archivo del usuario como perteneciente a 0:0 dentro del contenedor\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h8-volume-permissions-b16a91d894e4363082f108e17d3593e6.png\" width=\"1196\" height=\"374\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">hello</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">cat: can't open '/data/theirs.txt': Permission denied</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">DENIED</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">0:0 /data/mine.txt</span><br></div></code></pre></div></div>\n<p>Tu propio archivo se lee sin problema. El de root se rechaza — aunque el contenedor se crea\nroot, porque el kernel sabe más. Y tu archivo aparece dentro del contenedor como propiedad de\n<code>0:0</code>, la misma traducción funcionando al revés.</p>\n<p>Conviene interiorizarlo antes de mover una base de datos y pasarte una hora preguntándote por\nqué no puede abrir su propio directorio de datos.</p>\n<p>Una cosa más: el demonio rootless mantiene un <strong>almacén de imágenes separado</strong> en\n<code>~/.local/share/docker</code>. Las imágenes que ya tiene tu demonio del sistema son invisibles para\nél, y todo se descarga otra vez. Con el disco justo, tenlo en cuenta.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"muro-4--quitarlo-también-se-niega\">Muro 4 — quitarlo también se niega<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/#muro-4--quitarlo-tambi%C3%A9n-se-niega\" class=\"hash-link\" aria-label=\"Enlace directo al Muro 4 — quitarlo también se niega\" title=\"Enlace directo al Muro 4 — quitarlo también se niega\" translate=\"no\">​</a></h2>\n<p>Por simetría, la desinstalación tiene la misma protección:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable environment constant\" style=\"color:#36acaa\">PATH</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">/home/ubuntu/bin:</span><span class=\"token environment constant\" style=\"color:#36acaa\">$PATH</span><span class=\"token plain\"> ~/bin/dockerd-rootless-setuptool.sh uninstall</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El script de desinstalación aborta porque el Docker rootful está en marcha y accesible, e indica al usuario que use --force\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h8-uninstall-refuses-9d0ed4f387ceb4f76df893f91156189a.png\" width=\"1188\" height=\"142\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">[ERROR] Aborting because rootful Docker (/var/run/docker.sock) is running and accessible.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Set --force to ignore.</span><br></div></code></pre></div></div>\n<p>Añade <code>--force</code> cuando de verdad quieras quitarlo.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-te-da-esto-y-qué-no\">Qué te da esto y qué no<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/#qu%C3%A9-te-da-esto-y-qu%C3%A9-no\" class=\"hash-link\" aria-label=\"Enlace directo al Qué te da esto y qué no\" title=\"Enlace directo al Qué te da esto y qué no\" translate=\"no\">​</a></h2>\n<p>Cambia el peor escenario de <em>\"un atacante es dueño de la máquina\"</em> a <em>\"un atacante es dueño de\nesta cuenta de usuario\"</em>. En un host donde todos los servicios corren como <code>ubuntu</code>, eso es un\navance grande y no uno completo — tus claves SSH, tus archivos <code>.env</code> y tu código siguen dentro\nde esa cuenta.</p>\n<p>Así que el resumen honesto: rootless sube el suelo. No vuelve segura la habitación. Estrechar\nlo que la propia cuenta puede alcanzar es el siguiente peldaño, y ahí es donde entran el filtrado\nde llamadas al sistema y la cadena de suministro de imágenes.</p>\n<p>Pero lo concreto que era cierto al principio de este post — que cualquiera capaz de ejecutar un\ncontenedor podía quedarse con la máquina entera — ya no lo es, y puedes demostrarlo con un\ncomando.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Move the daemon off root</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"más-lectura\">Más lectura<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/#m%C3%A1s-lectura\" class=\"hash-link\" aria-label=\"Enlace directo al Más lectura\" title=\"Enlace directo al Más lectura\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.docker.com/engine/security/rootless/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker — Run the daemon as a non-root user (rootless mode)</a></li>\n<li class=\"\"><a href=\"https://docs.docker.com/engine/security/userns-remap/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker — Isolate containers with a user namespace</a></li>\n<li class=\"\"><a href=\"https://github.com/rootless-containers/rootlesskit\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">rootlesskit</a></li>\n<li class=\"\"><a href=\"https://man7.org/linux/man-pages/man5/subuid.5.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><code>subuid(5)</code></a> y <a href=\"https://man7.org/linux/man-pages/man7/user_namespaces.7.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><code>user_namespaces(7)</code></a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/",
            "title": "Mueve el demonio de Docker fuera de root para que una fuga del contenedor caiga en un usuario común",
            "summary": "Si puedes ejecutar docker sin sudo, ya puedes leer cualquier archivo de la máquina. No por un fallo — así está construido Docker. Un solo comando lo demuestra. En un host que ejecuta agentes que corren código generado, ese es el ataque completo, y los contenedores no-root de la Parte 2 no lo tocan. Esto mueve el propio demonio a una cuenta sin privilegios, con la aritmética de UID que puedes comprobar, cuatro puntos donde la instalación te lleva contra un muro en Ubuntu, y qué deja de funcionar después.",
            "date_modified": "2026-09-16T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "seguridad",
                "docker",
                "rootless",
                "contenedores",
                "privilegios",
                "agentes",
                "autoalojamiento",
                "endurecimiento"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/",
            "content_html": "<div class=\"tutorialHero\"><svg viewBox=\"0 0 24 24\" fill=\"#2496ED\" aria-label=\"Docker\" class=\"tutorialHero__docker\"><path d=\"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z\"></path></svg><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Lock down Docker networks</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run containers as non-root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Membership, not just an account</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">An identity for the agent, not a key</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">A tool server that checks who is asking</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">7</span><span class=\"skillTracker__skill\" data-state=\"current\">Where the agent can go, not just what it can call</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/\"><span class=\"skillTracker__dot\" data-state=\"locked\">8</span><span class=\"skillTracker__skill\" data-state=\"locked\">Move the daemon off root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/\"><span class=\"skillTracker__dot\" data-state=\"locked\">9</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run the model's own code without trusting it</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">A scope per tool, and a refusal clients can act on</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>La <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/\">parte 4 de orquestación de agentes</a> dividió una caja\nde herramientas para que el agente de agenda no pudiera emitir reembolsos. La\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/\">parte 5</a> le dio al agente una identidad\npropia en el gateway. La <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/\">parte 6</a> puso un verificador de JWT\ndelante del servidor de herramientas para que rechace a quien llama sin identificarse.</p>\n<p>Todas ellas controlan <strong>qué puede llamar el agente</strong>. Ninguna controla <strong>adónde puede ir</strong>.</p>\n<p>Esa distinción es todo este artículo. La exfiltración no necesita una herramienta. Necesita un\nsocket.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>Una WEC Instance: 8 vCPU AMD EPYC 7601, 15 GB de RAM, Ubuntu 22.04, kernel 5.15. Docker\n<code>29.1.3</code> con backend de firewall <code>iptables</code> — que en esta distribución es <code>iptables-nft</code>\nv1.8.7, una capa que en realidad escribe en nftables. El contenedor de pruebas es\n<code>alpine:3.20</code>. UFW está activo.</p><p>Cada código de estado, cada mensaje de error y cada medición de este artículo salen de la\nejecución descrita. HTTP plano en una LAN privada donde aparece la LAN, como en toda la serie.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"dos-direcciones-y-solo-una-suele-estar-vigilada\">Dos direcciones, y solo una suele estar vigilada<a href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/#dos-direcciones-y-solo-una-suele-estar-vigilada\" class=\"hash-link\" aria-label=\"Enlace directo al Dos direcciones, y solo una suele estar vigilada\" title=\"Enlace directo al Dos direcciones, y solo una suele estar vigilada\" translate=\"no\">​</a></h2>\n<p>Un firewall decide qué paquetes pueden cruzar un límite. Casi siempre la gente lo dice en una\nsola dirección — <strong>entrante</strong>: quién desde fuera puede llegar <em>hasta</em> tu servicio. Eso es lo que\nhace publicar un puerto, y de eso trataba la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/\">parte 1 de esta serie</a>.</p>\n<p><strong>La salida es la otra dirección</strong>: adónde puede llegar tu servicio. A qué direcciones puede\nabrir una conexión y a cuáles no.</p>\n<p>Casi nadie configura la salida, y para una aplicación web normal es una decisión razonable.\nHabla con su base de datos y con un proveedor de pagos, hace lo que dice su código, y ese código\nno cambia entre despliegues.</p>\n<p>Un agente no es eso. Un agente decide qué hacer en tiempo de ejecución, a partir de texto que\nacaba de leer — un correo, un ticket de soporte, una página web, un documento que escribió otra\npersona. Si un atacante controla ese texto, influye en lo que el agente hace a continuación. Y\nlo que no quieres que haga a continuación es <strong>exfiltración</strong>: enviar tus datos a donde no\ndeben ir.</p>\n<p>La exfiltración no necesita ninguna herramienta especial. Cualquier proceso que pueda abrir un\nsocket puede enviar cualquier cosa que pueda leer. Por eso la dirección que nadie vigila es\njusto la que importa aquí.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-dicen-realmente-los-estándares\">Lo que dicen realmente los estándares<a href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/#lo-que-dicen-realmente-los-est%C3%A1ndares\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que dicen realmente los estándares\" title=\"Enlace directo al Lo que dicen realmente los estándares\" translate=\"no\">​</a></h2>\n<p>La referencia principal para este tipo de problema es <strong>LLM06:2025 Agencia Excesiva</strong> de OWASP.\nSu sección de prevención enumera, en orden: <em>minimizar las extensiones</em>, <em>minimizar la\nfuncionalidad de las extensiones</em>, <em>evitar extensiones abiertas</em>, y algunos puntos más sobre\npermisos y aprobación humana.</p>\n<p>Lee la página buscando la red y no está. Ni \"egress\", ni \"firewall\", ni \"red\", ni \"aislamiento\",\nni \"sandbox\" — esas palabras no aparecen en la entrada.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La página de OWASP LLM06 Agencia Excesiva mostrando sus estrategias de prevención y mitigación, todas sobre herramientas y extensiones\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h7-owasp-mitigations-d1025852bd1958d91bfe4304dd74c563.png\" width=\"2551\" height=\"1376\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Cada mitigación de esa lista opera sobre <strong>herramientas</strong>. Quita la extensión, acota la\nextensión, evita la extensión abierta. Todo es buen consejo. Nada de eso cambia un solo byte de\nlo que un proceso comprometido puede hacer con un socket TCP.</p>\n<p>Hay quien defiende que esto es un hueco y no un descuido, y el argumento se está haciendo dentro\ndel propio repositorio de OWASP. Un\n<a href=\"https://github.com/OWASP/www-project-top-10-for-large-language-model-applications/issues/802\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">issue abierto que propone un mapeo de aplicación en tiempo de ejecución</a>\npara el Top 10 Agéntico lo dice sin rodeos:</p>\n<blockquote>\n<p>The enforcement pipeline assumes agents cannot reach tools without passing through the\nproxy. If tools are directly reachable, runtime enforcement collapses.</p>\n</blockquote>\n<p>y concluye que <em>\"Network-level controls (e.g., Kubernetes NetworkPolicy, Docker network\nisolation) are required to close this gap in production.\"</em></p>\n<p>Fíjate en qué es eso: un <strong>issue abierto</strong>, presentado por un colaborador, con su mapeo marcado\ncomo <em>(Proposed)</em>. No es el estándar. Es alguien argumentando que el estándar debería decir esto.</p>\n<p>Y la razón por la que esto importa no es teórica. <strong>EchoLeak</strong>\n(<a href=\"https://nvd.nist.gov/vuln/detail/CVE-2025-32711\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">CVE-2025-32711</a>) es una inyección de prompt\nindirecta de cero clics contra Microsoft 365 Copilot, registrada en NVD como <em>\"Ai command\ninjection in M365 Copilot allows an unauthorized attacker to disclose information over a\nnetwork.\"</em> Over a network. La lista de herramientas nunca fue lo que falló.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">Docker en Linux con el backend de firewall <code>iptables</code> — el predeterminado. Compruébalo con\n<code>docker info | grep -i \"Firewall Backend\"</code></li>\n<li class=\"\"><code>sudo</code>, y voluntad de escribir reglas de firewall en una máquina que aún puedas alcanzar</li>\n<li class=\"\">Nada de las partes anteriores. Esta se sostiene sola.</li>\n</ul>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Lee esto antes de tocar DOCKER-USER</div><div class=\"admonitionContent_BuS1\"><p><code>DOCKER-USER</code> es estado real del firewall de tu host. Una regla que coincida con más de lo que\npretendías dejará servicios fuera de línea. Todas las reglas de este artículo están acotadas con\n<code>-i br-agent</code>, una interfaz que pertenece a una red creada para este fin — que es justo por lo\nque en el paso 1 nombramos ese bridge a mano en lugar de dejar que Docker genere uno.</p><p>Si trabajas por SSH, ninguna de estas reglas toca la cadena <code>INPUT</code>, así que tu sesión no corre\nriesgo. Compruébalo tú mismo antes de creértelo.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--demuestra-el-hueco\">Paso 1 — Demuestra el hueco<a href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/#paso-1--demuestra-el-hueco\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Demuestra el hueco\" title=\"Enlace directo al Paso 1 — Demuestra el hueco\" translate=\"no\">​</a></h2>\n<p>Una red, con la interfaz del bridge nombrada explícitamente. Docker documenta la opción como\n<em>\"Interface name to use when creating the Linux bridge\"</em>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> network create </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--driver</span><span class=\"token plain\"> bridge </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">com.docker.network.bridge.name</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">br-agent agent-egress</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">ip</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-br</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">link</span><span class=\"token plain\"> show br-agent</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">br-agent         DOWN           8a:77:65:54:9b:0d &lt;NO-CARRIER,BROADCAST,MULTICAST,UP&gt;</span><br></div></code></pre></div></div>\n<p><strong>DOWN</strong>, porque todavía no hay nada conectado. Una red de Docker es una interfaz que no se\nlevanta hasta que se conecta un contenedor, y una regla con <code>-i br-agent</code> sobre una red inactiva\nno coincide con nada, en silencio. Conviene saberlo antes de pasar una hora depurando una\npolítica que nunca se ejecutó.</p>\n<p>El contenedor de pruebas — con versión fijada, no <code>latest</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--name</span><span class=\"token plain\"> agent-sandbox </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--network</span><span class=\"token plain\"> agent-egress alpine:3.20 </span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> infinity</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> agent-sandbox apk </span><span class=\"token function\" style=\"color:#d73a49\">add</span><span class=\"token plain\"> --no-cache </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> bind-tools</span><br></div></code></pre></div></div>\n<p>Ahora cuatro destinos. Fíjate en lo que <em>no</em> hay en este contenedor: ni agente, ni framework, ni\ncliente MCP, ni herramientas. Aquí no se está evadiendo el acotado de herramientas — es que no\nhay nada que evadir.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> agent-sandbox </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'inference API   HTTP %{http_code}\\n'</span><span class=\"token plain\"> https://inference.wiline.com/v1/models</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> agent-sandbox </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'arbitrary host  HTTP %{http_code}\\n'</span><span class=\"token plain\"> https://example.com</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> agent-sandbox </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'authentik LAN   HTTP %{http_code}\\n'</span><span class=\"token plain\"> http://10.80.4.212:9100/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> agent-sandbox </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'other network   exit=%{exitcode} %{errormsg}\\n'</span><span class=\"token plain\"> http://172.21.0.2:5432/</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">inference API   HTTP 401</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">arbitrary host  HTTP 200</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">authentik LAN   HTTP 302</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">other network   exit=28 Connection timed out after 5002 milliseconds</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Cuatro sondas desde el contenedor de pruebas: la API de inferencia responde 401, example.com responde 200, authentik responde 302, y una conexión a otra red Docker expira\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h7-before-unrestricted-527fc904745f99b3fdab1b9d08035428.png\" width=\"1234\" height=\"444\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>El <code>401</code> es el que conviene entender. No se envió ninguna clave de API, así que la petición\nnunca llega a nada que pudiera cobrarse — pero un <code>401</code> es una respuesta HTTP, lo que significa\nque el handshake TCP se completó, que TLS negoció, y que respondió un servidor real.\nAlcanzabilidad demostrada, gratis.</p>\n<p>La cuarta línea es la que mantiene esto honesto. Ese es un contenedor de Postgres en otra red de\nDocker, y <strong>expira</strong> — la <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/\">parte 1 de esta serie</a>\nestableció que las redes bridge separadas sí aíslan, y esto lo confirma.</p>\n<p>Así que la forma del hueco es precisa, no vaga:</p>\n<table><thead><tr><th>Destino</th><th>Resultado</th><th></th></tr></thead><tbody><tr><td>Otra red de Docker</td><td>expira</td><td>Docker ya se encarga de esto</td></tr><tr><td>Tu propia LAN, incluido el proveedor de identidad</td><td><code>302</code></td><td>abierto</td></tr><tr><td>Internet abierto</td><td><code>200</code></td><td>abierto</td></tr><tr><td>La única API que de verdad necesita</td><td><code>401</code></td><td>abierto</td></tr></tbody></table>\n<p>Docker aísla los contenedores <strong>entre sí</strong> y ahí se detiene. Las dos direcciones que importan\npara la exfiltración — tu red e internet — están abiertas de par en par, y ninguna guía de\nDocker lo presenta como un hueco porque, para una aplicación web, no lo es.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--el-suelo\">Paso 2 — El suelo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/#paso-2--el-suelo\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — El suelo\" title=\"Enlace directo al Paso 2 — El suelo\" translate=\"no\">​</a></h2>\n<p>Una regla. Fíjate en <code>-A</code> y no <code>-I</code>: la denegación va al <strong>final</strong> de la cadena y se queda ahí,\ny cada permiso que añadamos después se apila encima. Ese orden <em>es</em> la política.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-A</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> br-agent </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-j</span><span class=\"token plain\"> DROP</span><br></div></code></pre></div></div>\n<p><code>DOCKER-USER</code> es donde esto corresponde, y la documentación de Docker dice por qué:</p>\n<blockquote>\n<p>Rules appended to the <code>FORWARD</code> chain will be processed after Docker's rules.</p>\n</blockquote>\n<p><code>DOCKER-USER</code> se procesa <em>antes</em> que ellas — la documentación la describe como <em>\"a placeholder\nfor user-defined rules that will be processed before rules in the <code>DOCKER-FORWARD</code> and <code>DOCKER</code>\nchains.\"</em> En cualquier otro sitio, el <code>ACCEPT</code> del propio Docker llega primero.</p>\n<!-- -->\n<p>Léelo de arriba abajo y la colocación se explica sola. <code>DOCKER-FORWARD</code> contiene un <code>ACCEPT</code>\nincondicional por cada bridge — eso es lo que hace que la salida de los contenedores funcione, y\npor eso una regla añadida a <code>FORWARD</code> nunca se dispara. <code>DOCKER-USER</code> es el único punto que\nqueda por delante.</p>\n<p>Repite las mismas cuatro sondas:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">inference API   exit=28 Resolving timed out after 8000 milliseconds</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">arbitrary host  exit=28 Resolving timed out after 8002 milliseconds</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">authentik LAN   exit=28 Connection timed out after 8003 milliseconds</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">other network   exit=28 Connection timed out after 8002 milliseconds</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La única regla DROP listada, y las cuatro sondas fallando con dos mensajes de error distintos\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h7-default-deny-dd96eebe9dcfc37644f95bf30a45d1ae.png\" width=\"1234\" height=\"720\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Lee el texto del error, no el fallo.</strong> Dos mensajes distintos, y la diferencia es información\nde diagnóstico gratuita:</p>\n<ul>\n<li class=\"\"><strong>\"Resolving timed out\"</strong> — el propio DNS quedó bloqueado. Nunca llegamos a saber la dirección.</li>\n<li class=\"\"><strong>\"Connection timed out\"</strong> — la dirección ya se conocía, el paquete salió, y el DROP lo cazó.</li>\n</ul>\n<p>Las dos sondas por nombre murieron en la resolución; las dos por IP murieron en el socket. Lo\nque nos dice algo que no es obvio: <strong>el resolvedor integrado de Docker reenvía sus consultas a\ntravés de <code>DOCKER-USER</code>.</strong> El resolvedor vive en <code>127.0.0.11</code> dentro del espacio de nombres del\ncontenedor, y es tentador suponer que ese tráfico nunca toca la ruta de forwarding del host. Sí\nla toca.</p>\n<p>Fíjate también en que cada fallo tardó los <strong>ocho segundos completos</strong>. Un <code>DROP</code> produce\nexpiraciones; un <code>REJECT</code> falla al instante. Sigilo contra facilidad de depuración, y este\nartículo elige el sigilo — pero si todavía estás iterando sobre una política, <code>REJECT</code> te\nahorrará muchísima espera.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--la-lista-de-permitidos\">Paso 3 — La lista de permitidos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/#paso-3--la-lista-de-permitidos\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — La lista de permitidos\" title=\"Enlace directo al Paso 3 — La lista de permitidos\" translate=\"no\">​</a></h2>\n<p>Antes de escribir una regla para el resolvedor, averigua qué resolvedor usa realmente el\ncontenedor:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> agent-sandbox </span><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> /etc/resolv.conf</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">nameserver 127.0.0.11</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">search corp.internal internal mesh.example.com</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">options edns0 trust-ad ndots:0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"># Based on host file: '/etc/resolv.conf' (internal resolver)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"># ExtServers: [8.8.8.8 8.8.4.4]</span><br></div></code></pre></div></div>\n<p><code>ExtServers: [8.8.8.8 8.8.4.4]</code>. <strong>El contenedor no está usando el DNS de esta red.</strong> El host\nresuelve a través de <code>systemd-resolved</code> en <code>127.0.0.53</code>, que es una dirección de loopback y por\ntanto inútil dentro del espacio de nombres de un contenedor, así que Docker sustituyó el DNS\npúblico de Google sin mencionarlo.</p>\n<p>Mira los dominios de búsqueda heredados en el mismo bloque (aquí redactados, pero reales en tu\nmáquina). Cada nombre interno que ese contenedor resuelve se le está preguntando a <code>8.8.8.8</code>,\ncon el sufijo de los dominios privados que heredó del host. Nadie eligió eso, y nada lo saca a\nla luz salvo leer este fichero.</p>\n<p>También decide la regla: permite el resolvedor que el contenedor <strong>tiene</strong>, no el que supusiste.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">getent hosts inference.wiline.com</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">67.207.107.229  inference.wiline.com</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-I</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> br-agent </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> udp </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--dport</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">53</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8.8</span><span class=\"token plain\">.8.8,8.8.4.4 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-j</span><span class=\"token plain\"> ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-I</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> br-agent </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> tcp </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--dport</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">53</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8.8</span><span class=\"token plain\">.8.8,8.8.4.4 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-j</span><span class=\"token plain\"> ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-I</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> br-agent </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> tcp </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--dport</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">443</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">67.207</span><span class=\"token plain\">.107.229 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-j</span><span class=\"token plain\"> ACCEPT</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-S</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-N DOCKER-USER</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-A DOCKER-USER -d 67.207.107.229/32 -i br-agent -p tcp -m tcp --dport 443 -j ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-A DOCKER-USER -d 8.8.4.4/32 -i br-agent -p tcp -m tcp --dport 53 -j ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-A DOCKER-USER -d 8.8.8.8/32 -i br-agent -p tcp -m tcp --dport 53 -j ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-A DOCKER-USER -d 8.8.4.4/32 -i br-agent -p udp -m udp --dport 53 -j ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-A DOCKER-USER -d 8.8.8.8/32 -i br-agent -p udp -m udp --dport 53 -j ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-A DOCKER-USER -i br-agent -j DROP</span><br></div></code></pre></div></div>\n<p>Cinco permisos sobre una denegación. Las mismas cuatro sondas, por tercera vez:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">inference API   HTTP 401 exit=0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">arbitrary host  exit=28 Connection timed out after 8002 milliseconds</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">authentik LAN   exit=28 Connection timed out after 8003 milliseconds</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">other network   exit=28 Connection timed out after 8001 milliseconds</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Las cuatro sondas tras la lista de permitidos: la API de inferencia responde 401, y los otros tres destinos expiran en la fase de conexión y no en la de DNS\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h7-allowlist-applied-71e84e955ac296d1b262e120254f5409.png\" width=\"1240\" height=\"560\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Mira la segunda línea.</strong> Dice <em>\"Connection timed out\"</em>, no <em>\"Resolving timed out\"</em>. El DNS está\nabierto, así que <code>example.com</code> sí resolvió a una dirección real — y entonces el paquete murió.\nLa resolución funcionó y el socket no.</p>\n<p>Ese es todo el artículo en una línea. Un proceso comprometido dentro de ese contenedor puede\naveriguar adónde enviar tus datos y no puede enviarlos.</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>La regla de conntrack que esto no necesita</div><div class=\"admonitionContent_BuS1\"><p>Todas las guías sobre este tema te dicen que primero aceptes el tráfico <code>RELATED,ESTABLISHED</code>, y\nla documentación de Docker muestra la regla. Esta política no tiene ninguna, y el <code>401</code> de\narriba demuestra que el handshake TLS completo y la respuesta HTTP volvieron igualmente.</p><p>La razón es la dirección. Nuestro DROP coincide con <code>-i br-agent</code> — tráfico que llega <strong>desde</strong>\nel contenedor. Los paquetes de vuelta llevan <code>-o br-agent</code>, nunca coinciden, atraviesan\n<code>DOCKER-USER</code> sin tocarse, y los aceptan después las propias reglas de seguimiento de conexiones\nde Docker.</p><p>Necesitas el <code>ACCEPT</code> de conntrack cuando tu DROP puede coincidir con la dirección de vuelta, que\nes el caso de los ejemplos de tráfico entrante de donde viene ese consejo. Un DROP de salida\nespecífico de dirección, no. Añádela igualmente si quieres — no cuesta nada — pero entiende que\ncopiarla sin entenderla es como se llega a la conclusión de que las reglas de firewall son magia\nnegra.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--medir-el-coste-dos-veces\">Paso 4 — Medir el coste, dos veces<a href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/#paso-4--medir-el-coste-dos-veces\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — Medir el coste, dos veces\" title=\"Enlace directo al Paso 4 — Medir el coste, dos veces\" translate=\"no\">​</a></h2>\n<p>Una política que nadie mide es una política que alguien desactivará cuando haya carga. Así que:\n¿cuánto cuestan seis reglas?</p>\n<p>El enfoque obvio, veinte conexiones con las reglas puestas y veinte sin ellas:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> </span><span class=\"token for-or-select variable\" style=\"color:#36acaa\">i</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">seq</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable number\" style=\"color:#36acaa\">20</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> agent-sandbox </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'%{time_connect}\\n'</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8</span><span class=\"token plain\"> https://inference.wiline.com/v1/models</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">done</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sort</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-n</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">awk</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{a[NR]=$1} END{printf \"rules ON  — median %.4fs  min %.4fs  max %.4fs  (n=%d)\\n\", a[int(NR/2)+1], a[1], a[NR], NR}'</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-F</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">rules ON  — median 0.1573s  min 0.0940s  max 0.3768s  (n=20)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">rules OFF — median 0.0936s  min 0.0911s  max 0.1576s  (n=20)</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La medición ingenua mostrando una diferencia de mediana de 64 milisegundos entre reglas activas y reglas desactivadas\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h7-overhead-naive-f360e40c29ad6c2b0c7ff16a3663104c.png\" width=\"1230\" height=\"376\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>64 milisegundos.</strong> Lo cual sería un hallazgo serio, si fuera cierto.</p>\n<p>No lo es. Seis reglas de coincidencia de paquetes son microsegundos de trabajo del kernel, y los\nmínimos delatan el truco: <code>0.0940</code> frente a <code>0.0911</code>, casi idénticos. La ruta rápida es la misma\nen ambas ejecuciones. Toda la diferencia vive en la cola.</p>\n<p>El <code>time_connect</code> de <code>curl</code> se mide desde el inicio de la petición y por tanto <strong>incluye la\nresolución del nombre</strong>. La primera ejecución pagó una ida y vuelta completa a <code>8.8.8.8</code>. Para\nla segunda, el resolvedor ya tenía la respuesta en caché. Medimos el orden en que las\nejecutamos.</p>\n<p>Mide solo la conexión TCP, restando la resolución:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> </span><span class=\"token for-or-select variable\" style=\"color:#36acaa\">i</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">seq</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable number\" style=\"color:#36acaa\">30</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> agent-sandbox </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'%{time_namelookup} %{time_connect}\\n'</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8</span><span class=\"token plain\"> https://inference.wiline.com/v1/models</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">done</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">awk</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{print ($2-$1)}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sort</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-n</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">awk</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{a[NR]=$1} END{printf \"connect-only median %.5fs  min %.5fs  max %.5fs  (n=%d)\\n\", a[int(NR/2)+1], a[1], a[NR], NR}'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">rules ON  — connect-only median 0.07098s  min 0.06999s  max 0.08607s  (n=30)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">rules OFF — connect-only median 0.07102s  min 0.06996s  max 0.07214s  (n=30)</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La medición corregida mostrando una diferencia de mediana de cuatro centésimas de milisegundo, con la ruta protegida por el firewall nominalmente más rápida\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h7-overhead-94f3b889647c2c273680e82c050b345a.png\" width=\"1240\" height=\"452\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Una diferencia de mediana de −0,04 ms.</strong> Negativa: la ruta protegida midió marginalmente <em>más\nrápido</em>, que es el aspecto que tiene el ruido. Todo cae dentro de unos ±40 µs.</p>\n<p>Así que la respuesta honesta no es un número con un signo más delante. Es que el coste de esta\npolítica está por debajo de lo que este método puede resolver — y que la versión ingenua de la\nmisma medición se equivocaba por tres órdenes de magnitud, en la dirección que te habría\nconvencido de no desplegarla.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5--sobrevivir-a-un-reinicio\">Paso 5 — Sobrevivir a un reinicio<a href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/#paso-5--sobrevivir-a-un-reinicio\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 5 — Sobrevivir a un reinicio\" title=\"Enlace directo al Paso 5 — Sobrevivir a un reinicio\" translate=\"no\">​</a></h2>\n<p>Las reglas de <code>iptables</code> viven en memoria. Ese <code>-F</code> del paso anterior le hizo a la política\nexactamente lo mismo que le hace un reinicio.</p>\n<p>El reflejo es <code>iptables-persistent</code>, y aquí es la herramienta equivocada: guarda la tabla entera\nincluidas todas las reglas que generó Docker, y Docker vuelve a generarlas él mismo al arrancar.\nAcabas con duplicados y con un orden que nadie eligió.</p>\n<p>Lo que quieres es una unidad que sea dueña <strong>solo de tus reglas</strong> y que se ejecute <strong>después</strong> de\nDocker:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">/usr/local/sbin/agent-egress.sh</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token shebang important\">#!/usr/bin/env bash</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Lista de permitidos de salida para el bridge del agente. Se reaplica tras arrancar Docker.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-euo</span><span class=\"token plain\"> pipefail</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">BR</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">br-agent</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">DNS</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"8.8.8.8 8.8.4.4\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">API</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">getent hosts inference.wiline.com </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">awk</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'{print $1; exit}'</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Elimina solo nuestras propias reglas, para que sea seguro ejecutarlo dos veces.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># `|| true` importa: con la cadena limpia, grep sale con 1 y abortaría el script.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">existing</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">iptables </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-S</span><span class=\"token variable\" style=\"color:#36acaa\"> DOCKER-</span><span class=\"token variable environment constant\" style=\"color:#36acaa\">USER</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">grep</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">\" -i </span><span class=\"token variable string variable\" style=\"color:#36acaa\">${BR}</span><span class=\"token variable string\" style=\"color:#e3116c\"> \"</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable operator\" style=\"color:#393A34\">||</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable boolean\" style=\"color:#36acaa\">true</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-n</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">${existing}</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">then</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token builtin class-name\">printf</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'%s\\n'</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">${existing}</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sed</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'s/^-A /-D /'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">while</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">read</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> rule</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"> iptables </span><span class=\"token variable\" style=\"color:#36acaa\">${rule}</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">||</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">true</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">done</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">fi</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> </span><span class=\"token for-or-select variable\" style=\"color:#36acaa\">d</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">${DNS}</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-I</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">${BR}</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> udp </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--dport</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">53</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">${d}</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-j</span><span class=\"token plain\"> ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-I</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">${BR}</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> tcp </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--dport</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">53</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">${d}</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-j</span><span class=\"token plain\"> ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">done</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-I</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">${BR}</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> tcp </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--dport</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">443</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">${API}</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-j</span><span class=\"token plain\"> ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-A</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">${BR}</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-j</span><span class=\"token plain\"> DROP</span><br></div></code></pre></div></div>\n<div class=\"language-ini codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">/etc/systemd/system/agent-egress.service</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-ini codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">[Unit]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Description=Egress allowlist for the agent bridge</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">After=docker.service</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Requires=docker.service</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">[Service]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Type=oneshot</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">RemainAfterExit=yes</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ExecStart=/usr/local/sbin/agent-egress.sh</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">[Install]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WantedBy=multi-user.target</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">chmod</span><span class=\"token plain\"> +x /usr/local/sbin/agent-egress.sh</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> systemctl daemon-reload </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> systemctl </span><span class=\"token builtin class-name\">enable</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--now</span><span class=\"token plain\"> agent-egress.service</span><br></div></code></pre></div></div>\n<p>Demuestra que es idempotente reiniciándola y comparando la cadena consigo misma:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> systemctl restart agent-egress.service </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-S</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Las mismas seis reglas listadas tras dos reinicios consecutivos de la unidad, idénticas las dos veces\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h7-persistent-unit-08c4df2a28634e493a6a37476e9429fb.png\" width=\"1230\" height=\"610\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Seis reglas, dos veces, sin duplicados.</p>\n<p>Dos cosas que esto te da de forma discreta. <code>getent</code> se ejecuta en cada arranque, así que <strong>la\ndirección de la API se vuelve a resolver al iniciar</strong> — no de forma continua, pero sí la mayor\nparte barata del problema del destino móvil, sin ningún demonio extra. Y el bucle de borrado\nelimina solo las reglas que coinciden con <code>-i br-agent</code>, así que la unidad nunca toca nada más\nde la cadena.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes poner una política de denegación por defecto delante de un contenedor, permitir\nexactamente los destinos que necesita, distinguir un fallo de DNS de un fallo de socket leyendo\nel error, medir el coste sin engañarte, y hacer que todo sobreviva a un reinicio.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"si-cambias-docker-al-backend-de-nftables\">Si cambias Docker al backend de nftables<a href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/#si-cambias-docker-al-backend-de-nftables\" class=\"hash-link\" aria-label=\"Enlace directo al Si cambias Docker al backend de nftables\" title=\"Enlace directo al Si cambias Docker al backend de nftables\" translate=\"no\">​</a></h2>\n<p>Docker 29 incluye un backend de firewall nftables junto al de iptables. No es el predeterminado y\neste artículo no lo ejecutó — el cambio reconstruye todas las reglas del host, y verificar el\ncomportamiento completo exige un reinicio. Lo que sigue viene de la documentación de Docker, y se\nmarca como tal.</p>\n<p>Todo lo anterior deja de aplicarse, porque:</p>\n<blockquote>\n<p>In Docker's nftables implementation, there is no <code>DOCKER-USER</code> chain.</p>\n</blockquote>\n<p>En su lugar creas tu propia tabla, y la prioridad hace el trabajo que hacía <code>DOCKER-USER</code>:</p>\n<blockquote>\n<p>If your rules need to run before Docker's rules, give the base chains a lower priority number\nthan Docker's chain.</p>\n</blockquote>\n<p>Y una regla que bajo iptables sería definitiva, no lo es:</p>\n<blockquote>\n<p>In nftables, an \"accept\" rule is not final. It terminates processing for its base chain, but\nthe accepted packet will still be processed by other base chains, which may drop it.</p>\n</blockquote>\n<p>Para sobreponerse al drop de Docker hace falta entonces <code>--bridge-accept-fwmark</code>.</p>\n<p>La trampa que conviene conocer es la transición, y es silenciosa en ambos sentidos:</p>\n<blockquote>\n<p>When starting the daemon with nftables after running with iptables, Docker will not remove the\njump from the <code>FORWARD</code> chain to <code>DOCKER-USER</code>. So, rules created in <code>DOCKER-USER</code> will\ncontinue to run until the jump is removed or the host is rebooted. When starting with nftables,\nthe daemon will not add the jump. So, unless there is an existing jump, rules in <code>DOCKER-USER</code>\nwill be ignored.</p>\n</blockquote>\n<p>Cambia de backend y tu política sigue funcionando — hasta el siguiente reinicio, cuando deja de\nhacerlo en silencio. <code>iptables -S DOCKER-USER</code> seguirá listando todas las reglas.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"resolución-de-problemas--los-errores-que-produjo-esta-ejecución\">Resolución de problemas — los errores que produjo esta ejecución<a href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/#resoluci%C3%B3n-de-problemas--los-errores-que-produjo-esta-ejecuci%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al Resolución de problemas — los errores que produjo esta ejecución\" title=\"Enlace directo al Resolución de problemas — los errores que produjo esta ejecución\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-unidad-de-systemd-falla-en-su-primera-ejecución-y-funciona-en-la-segunda\">La unidad de systemd falla en su primera ejecución y funciona en la segunda<a href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/#la-unidad-de-systemd-falla-en-su-primera-ejecuci%C3%B3n-y-funciona-en-la-segunda\" class=\"hash-link\" aria-label=\"Enlace directo al La unidad de systemd falla en su primera ejecución y funciona en la segunda\" title=\"Enlace directo al La unidad de systemd falla en su primera ejecución y funciona en la segunda\" translate=\"no\">​</a></h3>\n<p><code>status=1/FAILURE</code> y nada más en el journal. La causa es la línea de limpieza: con la cadena\nvacía, <code>grep</code> no encuentra reglas coincidentes y sale con <code>1</code>, y <code>set -euo pipefail</code> convierte\neso en un aborto antes de añadir ninguna regla. La guarda de idempotencia rompe justo la única\nejecución que no necesita idempotencia. Mira el <code>|| true</code> del paso 5.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"todo-expira-incluso-lo-que-permitiste\">Todo expira, incluso lo que permitiste<a href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/#todo-expira-incluso-lo-que-permitiste\" class=\"hash-link\" aria-label=\"Enlace directo al Todo expira, incluso lo que permitiste\" title=\"Enlace directo al Todo expira, incluso lo que permitiste\" translate=\"no\">​</a></h3>\n<p>Comprueba si el fallo dice <em>\"Resolving\"</em> o <em>\"Connection\"</em>. Si dice Resolving, el problema es tu\nregla de DNS y nunca se llegó a la regla del destino. Los contenedores a menudo no usan el\nresolvedor que esperas — lee <code>/etc/resolv.conf</code> dentro del contenedor, no en el host.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"las-reglas-están-listadas-pero-no-se-filtra-nada\">Las reglas están listadas pero no se filtra nada<a href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/#las-reglas-est%C3%A1n-listadas-pero-no-se-filtra-nada\" class=\"hash-link\" aria-label=\"Enlace directo al Las reglas están listadas pero no se filtra nada\" title=\"Enlace directo al Las reglas están listadas pero no se filtra nada\" translate=\"no\">​</a></h3>\n<p>Comprueba que el bridge esté levantado: <code>ip -br link show br-agent</code>. Una red sin contenedores\nconectados tiene la interfaz <code>DOWN</code>, y una regla con <code>-i</code> sobre ella no coincide con nada.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"todo-tarda-exactamente-ocho-segundos-en-fallar\">Todo tarda exactamente ocho segundos en fallar<a href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/#todo-tarda-exactamente-ocho-segundos-en-fallar\" class=\"hash-link\" aria-label=\"Enlace directo al Todo tarda exactamente ocho segundos en fallar\" title=\"Enlace directo al Todo tarda exactamente ocho segundos en fallar\" translate=\"no\">​</a></h3>\n<p>Eso es <code>DROP</code> comportándose correctamente. Usa <code>REJECT</code> mientras iteras si prefieres fallar\nrápido.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-medición-muestra-decenas-de-milisegundos-de-sobrecoste\">La medición muestra decenas de milisegundos de sobrecoste<a href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/#la-medici%C3%B3n-muestra-decenas-de-milisegundos-de-sobrecoste\" class=\"hash-link\" aria-label=\"Enlace directo al La medición muestra decenas de milisegundos de sobrecoste\" title=\"Enlace directo al La medición muestra decenas de milisegundos de sobrecoste\" translate=\"no\">​</a></h3>\n<p>Estás midiendo DNS. Usa <code>%{time_connect}</code> menos <code>%{time_namelookup}</code>, y sospecha de cualquier\nresultado donde los mínimos coincidan y las medianas no.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-te-dio-esto-y-qué-no\">Qué te dio esto y qué no<a href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/#qu%C3%A9-te-dio-esto-y-qu%C3%A9-no\" class=\"hash-link\" aria-label=\"Enlace directo al Qué te dio esto y qué no\" title=\"Enlace directo al Qué te dio esto y qué no\" translate=\"no\">​</a></h2>\n<p>Hecho: un contenedor que puede alcanzar exactamente un destino y un resolvedor; un intento de\nexfiltración que falla <em>después</em> de una resolución DNS correcta y no antes; una política que se\nreaplica sola tras un reinicio y que vuelve a resolver su destino al hacerlo; y un coste medido\nindistinguible de cero.</p>\n<p>Sin hacer:</p>\n<ul>\n<li class=\"\"><strong>El DNS sigue siendo un canal de exfiltración.</strong> Permitimos consultas a <code>8.8.8.8</code>, y se pueden\ncolar datos dentro de ellas. Cerrar eso significa un resolvedor propio y una regla que apunte\nsolo a él — que es otro artículo.</li>\n<li class=\"\"><strong>La lista de permitidos es una IP, y las IP se mueven.</strong> La unidad vuelve a resolver al\narrancar, que no es lo mismo que seguir a una CDN. Un proxy de reenvío que filtre por SNI de\nTLS es la respuesta honesta para un destino que rota; también es bastante más maquinaria.</li>\n<li class=\"\"><strong>Esto protege contenedores.</strong> Los agentes de la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/\">parte 3 de orquestación</a> y de la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/\">parte 4</a> corren como procesos del host, donde estas\nreglas no aplican. Moverlos a un contenedor es el requisito previo, y merece la pena por sí\nmismo.</li>\n<li class=\"\"><strong>El host no está restringido</strong>, ni tampoco ningún otro contenedor de esta máquina. Otros cinco\nbridges siguen teniendo un <code>ACCEPT</code> incondicional en <code>DOCKER-FORWARD</code>.</li>\n<li class=\"\"><strong>Nada de esto detiene la inyección.</strong> Detiene la salida de la carga útil. Son trabajos\ndistintos.</li>\n</ul>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Where the agent can go, not just what it can call</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-viene-después\">Qué viene después<a href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/#qu%C3%A9-viene-despu%C3%A9s\" class=\"hash-link\" aria-label=\"Enlace directo al Qué viene después\" title=\"Enlace directo al Qué viene después\" translate=\"no\">​</a></h2>\n<p>La secuela obvia es el resolvedor: monta uno propio, apunta el contenedor a él, permite el puerto\n53 solo hacia esa dirección, y el canal de DNS se cierra junto con la dependencia de Google.\nTambién prepara el terreno para hacer bien las listas de permitidos por dominio, que es la parte\ndel problema del destino móvil que este artículo deja abierta.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lectura-adicional\">Lectura adicional<a href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/#lectura-adicional\" class=\"hash-link\" aria-label=\"Enlace directo al Lectura adicional\" title=\"Enlace directo al Lectura adicional\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.docker.com/engine/network/packet-filtering-firewalls/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker — Packet filtering and firewalls</a></li>\n<li class=\"\"><a href=\"https://docs.docker.com/engine/network/firewall-iptables/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker — Docker with iptables</a></li>\n<li class=\"\"><a href=\"https://docs.docker.com/engine/network/firewall-nftables/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker — Docker with nftables</a></li>\n<li class=\"\"><a href=\"https://docs.docker.com/engine/network/drivers/bridge/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker — Bridge network driver options</a></li>\n<li class=\"\"><a href=\"https://genai.owasp.org/llmrisk/llm062025-excessive-agency/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">OWASP — LLM06:2025 Excessive Agency</a></li>\n<li class=\"\"><a href=\"https://nvd.nist.gov/vuln/detail/CVE-2025-32711\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">NVD — CVE-2025-32711 (EchoLeak)</a></li>\n<li class=\"\"><a href=\"https://nvd.nist.gov/vuln/detail/CVE-2024-8309\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">NVD — CVE-2024-8309 (LangChain)</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/",
            "title": "Limita la salida de un contenedor de agente para que solo alcance una API y nada más",
            "summary": "Todas las partes anteriores de esta serie controlan qué puede llamar un agente. Ninguna controla adónde puede ir. Las mitigaciones de OWASP para Agencia Excesiva son tres reglas sobre herramientas y no dicen nada sobre la red. Esto pone una política de denegación por defecto delante de un contenedor, permite exactamente una API, y demuestra el límite con una resolución que funciona y un paquete que muere. Incluye la regla de conntrack que todo el mundo te dice que añadas y que esta no necesita, y una medición equivocada por tres órdenes de magnitud.",
            "date_modified": "2026-09-15T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "seguridad",
                "docker",
                "redes",
                "iptables",
                "firewall",
                "agentes",
                "autoalojamiento",
                "endurecimiento"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__authentik\" src=\"data:image/png;base64,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\" alt=\"authentik\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__mcp\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoAAAAKACAMAAAA7EzkRAAAANlBMVEVMaXEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADisHBNAAAAEXRSTlMAyiq43BsP9wXrOkymXpWDcBHBhRYAAAAJcEhZcwAACxMAAAsTAQCanBgAABjNSURBVHja7Z0JgtU4EkTl3fKu+192oGmmoZuCspVyKqUXBwCX/b6WVCjDOZRIw3p2ffiUfHPs7cwrQ1Jaru2T7P2jqTvXkVeHoke+ffPhqZpzZSREz9WefYjU1O0MhOjRxHtE0weD6OnMewnR951B5mL0ac17E8Tlz5Y3iz4z+J0+pBHDIPrzym8LCdVfrAbR7/DrQmL5EwTRB1qb8IKmY+BVIy38/kKQURD9W20XXpS/2I6gH3e+W3hZ/cpbR39rvqbwvjrqgujb1rcPKppO5mGkMPsyD6Mf9r4+aGpjP1y1xiMoq1/4ChUPf33Q18FKsNbN7xmyUM92uM7dRxMy0bTzNSosvviQjzam4dp0hqzUYFCoa/m3hczk2Q2z/GMhiF5R24ccdfJlKqn+TSFPsRWBP111EFi+9nz5+0IgR8Ol6wpZi3IM/CkTyBjoKD9DIKqVPwgsWEcIEIjgj2oM/OWtja8Ff5zKIVey/eX3wpkAf7reGC5swp+uP5CLIvCne1WJYkwx/KVrfDV579kKo5f5+5LFde5LO3wv143tsl5H17MRQan5+5rA9aFhZX6S6fWb/4tlYAEaBflrzmVOm+3FMhD+Pjwfuz5t1ZvXQ4bBgw9onT+h62/9OdzNuhEhn2og/D0OmJEIvPFMwtXzN22P9wLj5anFwF8kAW1c92nPJAx/mo3Ex3NiEna031Bs4RzZhBpnllH++nya2Ee14accXSl/TZtJEEnH16yQP+kMj5hBkH1IffzJ964fn68Ee+4o1cZfkhYtz6fhi09aF39HZl2pKcWYUutzbVX6uDLEEFgTf0t+3hyGwHr4S9ss/OntFKrR1fDXZnk/iiEQ/nQJZBXoakg/eiO17RmB1AKr4G/I95YUxyHwp+oT40Q4f/4mG/x9qQc++aUQ5eXKjv/oh7x/K5jz4c9pNuufqMTAn5w2OnXAn2Zf8LFnGwJ/mn3pl/tPSYiSKzT+TSUX4eBAuBj+TOZy3J+Eez51kfxpJVSut5+U+3HwJ6kORwL8aaYStXeXrg2f25UWv6qainWwD4Y/zacfJmrRdfO3zbbiw6hFu6Lif7X5uz0EemypjH9O80iYQgzjn+xGmEJMtfzl0YG+wxTo6oz/PUyWMT0fHv5ENXsWgfD3dA+77ud5nNfavveXUAksgj8BY9Ny/OBm8d0+vuMLJDuphPjfaP7G6z9equl4OA56StHwd1Prr61825B+NGcXAn/Dh6UTv6a3BeJHqJ2/9Xdz5nG/uD1OtOioKX76Snv/5IG9teMsBP7kCsf3L5icbIPhz8n5H7q0i0C2wc5w/Pn+hv/m7iJzxJYPf6L+r7t9rHrqMPAn6T9s5pS7EHoUGY3/jY7/OFORfmJHqCF++j3+7jZ03mlUWcH4t77pf10TboOpRFfJ35HQuDxgyMpdgy3+wnRrDp45CoE/Yf/hvf9woklb0fGr0fGDqVv59ZzFwZ+s/+beIrABQPiT9X81AAh/MvGrD6PdALAQtSb5u3li2wEg8dOy/us+HYA0R6go/vex/zDhGhAA4U/aNgqA8Odk/V/UAeEvOv48xn+4p7ubzkmIMxI/rcjfvdu7nAUTfy7sv2kSumEAsIr48zj/15WwPxF2LPgTzpbeMaQ64qcl/a9HymbrWPLL5y/Sf+iHlK3y6U6Uf/y0Ln+3twm3/rsJPkqPP4/139yN/5onQoPhT9D/NSRt0ktvmMTarfPXpg18+noUPLfrfl3nsX3XcZ7Xvi4sD/Xjf9X5WxLHdTVd83GJfuq77VzpnWA3/lzD/xp55PMrDpvjWsg1tMifgv91CWk0NedKIyNb8dMq/uszJFRzMh9XxN+jj92EtOqPldkY/oSsMA9n4w0G4S/Rn/3Zpzs4Q847fnpR8v834S31F3uSdAOBNn9P/ddLeFH+YEuS6/g3Kfn/j/CuOuyEKfg7rPpfxym8rQYEs4uf1vNfX0FBDa7+vPjT87/OPqiIURD+nMTRY8RakO2IFH+nXf7mPuhpw7yVR/yvov/wCpqaTo5H9PnT9L+OPuiqX+CvYv7S+mA+uX4e4S/kG3+elr9h0gcw9Cvx0xnHnyf1/3chC20j/BnlL85/vYdMVOtKUJ0/Xf+X+g7kh+3wRfx0dfzlMgH//bcM8BcyjT9PxN8VspJfiF+tir92ygvA+Dx5+LPkfx37kJ2OGf7yjD+X52/uQobqRvirg7/XbdAvtXV3xE/b8H9dIVP5lvjLCvjbQ7aqYTNcPX/rlC+A8W+X+Gn4oxxTWvx5Tv7X9LrgL7/434r4KzoJp3r+dgP8FUxgC38hQKDd+F/4g8Aq48+l+AtmVGI1xjx/Vz38CVS74M8V5n/lVE6Vvx7+3nYmDMSfC8avWvdfK6ib4S+T+N8q+Ys/dCT+PBv+ErVAmHzffFXvJ4oxBfM3Zcdfv5372v5oYB7bZT+3fmIrXFz8ub7/+ufd6XYtH1vn5+XaPBuRouLP1f2HP76L6zPFgPbqpEbCDv6i41eL4a/bP/9THHchBi/4q9x//X0yPO9OBMMlcfVzauEP/r6Eezyqya0Ctz8b09XA3Tp/2v6bv7Q9H4XaLXomPiuOn9bmz+fA3xY3CQ7RY/ACf7X6X7++g/jP326VTsK7cqcI8/7XL48gcxax9DVOwubjfxen3f9FrIP4fE7V7YQrjZ8W5E+0c27bVFaOhr/MAhTmmAPBlfjp2vzXCRoURHji+pn4aRPx52L8NW1eXcku+DPFX5Nnq8j5cU3Qj/BXkf/1zG9pdFbE31G7//rK8HB+Goifxn+taU8/4A//teZm2MoQCH/Z98R4SKCNIbDm+HMj/D0tkk4j8dP4XzXHwAv+HP5XxZ2In+GvbP5ezEjYC7ynHs3fBX95F2sb4qfxX6tuFxf4w3/tFPP6DuKnHf5DsWLMVJAlwXz7x/r4ezRn7Ix/8Ke5DGzgr0j/tVJO73j/sYcS46fN87d4oznRexGnIdXHn2v3H45QV8AcDH+T3SaQ93fC2c3B5tvvWfe/vvz2LuLP8R9q7kM6+IM/p3gmnJcrUN1+ZJ2/Rn1JdXsIXOHP4X91ehc1D+LP4U90CLz5R/Twh/9VdSM8EL+aCX+hCP5ca3MRaL79KOPfw+OQg/EP/+v3r7Cfx9ZtX4Lj2vf+lAb+MuDv0s/iXQ7/U4Tc+vSfnL25SqD59o/W/Ydu3v+7Beyv8RVf4GKevx7+YgtAv65A9M/+sNWYLVo9/rx2/+v4ceWkG9LPwUft/NXuf/3t/c9H1YXNkh+h+vZ7mfuvn/g77u2DPfzhf5X9Cwc7ZyHVt98z0H/4gcejt7INrp4/E/7rfky7CNyJn8b/+vtpPu269oQ/+JOdJBcTdZjq2z8eZvyHd89rRwt1mNr5M+V/XVPuQhr4w/8lPEh12RcCa2//aMx/eDfV49bqYjLIn3r7x9r813vK8uZI/DT+V9lKzJ73UQj8mfO/9ikdWS38wd+fNCcsBC62+NNuf1an/7pNeDVugT/817KQDPnezKy9/aM6fw8/QCkA1t7+0az/dUl4Frfa4e/A/6r0AdoiADzgz6r/dS5hCib+3Kz/q3cFAAh/dv2HmzNfhiH+3LL/f3fWC9HwZ9r/Olg/iiP+3LT/tXPGzQjVt3807n9dk+62Z/iDP9lGzltehlTiz437r29vf5qs+pTbb/841u3/un9tzed0KYn4c+P8+SFtc5gO/uBPdv+9ZnQxvfr2ozX2Hz7zScxUb/+o7f+qsv9wl81RcPXtR6v0X9/s0dsSf16u/1plALh3EpzwWjD8TVX2vz4ziSuk/WOd/N0rQ6erwtB+r1L/4c3E1hP+4E/0A+xZBNXQ/jHW/zVYHQC6kMEmmPjzavsPD/f+cj/DH/xJLoCuoL8Hof1oxf7rPqjvQWj/GOrlb0ntNYQ/+g8LbkES5FXTftS6/zWGv/bujw3+8B9KLsC3oLwENG9/w/8a9b9PSbtuYT+CP9kBULoKWH37x8u4/zWSvzZxzxnsH65w/2HkBNS9/cLhD//148tI4kWYlfaPlftf+/Q3jrEf0X/YyRmQ9pzipwf817b5W4LmDEz7x8r9rw8mYMk9cMX2I/h7/AZW4qfxX8ssgB6dgPcz/OXQfrQA/+uzCvgJf/hfhfgb+/SNp2n/CH+iFqSuFP7wv2rz57ZX+q7maf+Inn9r978K8PeoBN/AH/wJTECPGdjzmH/hz/YA8PgRZGowa9A9fh97/K82+ZPpizp4Xf7mBv6M8udFjoEbXf7chv/QKH8yA+Cue/wZe/6F/1WPP5G7ILPX5e/C/2qVP5kt8K7LX6T/Df7i+Xv8CDJb4EaVv8gNUI//VY8/mUOQNigeP8VugPFfxwNwvBg+Jz0FnDqnj/hfs+BvkmmK2mnyt9bN32aZP6lwuEmRv7gFIP5XVf5katA3Izll//y4BSD+1/gPsL0Zfy11CU+uBn5W2/6xAP6krsKtevy1E/5rs/z5QRdAAf6iJmDz8dPG+ZNrhrCqnUGfxJ+HoLcAiuNP7i76ooV/zAQMf07l/tE/B1ByzTgGreG3qbb9o37/4eifgGRH3tsD0aRtgag+/jyev8hHEG1J3r1d/v/2C+zrjT/X9l9HP0In2hH61OAvYgdinr9Gn79GuQFShBtGiL9hIv7cKn+TdCRD8z5/z0/ha+dP4APEPoJ4MvX+Pn8t8edm+Tuk+buxH5Di7/EACH/xi58mZLQBuTcEik3+baXtH9X9h/H8NfKpmJ+uxPhFq/JD/LkYf7EtKJLw9zlfqBx/Lfwp7T6jLZCDS6NPfBs5/h6uAE/6D6vz17pU2l/k71kN8KD/cMH8/fH79K3a0YsQf6Hu/sMSj5CSvy+zsE/a/f+HOpRX2Pyb7z+cAX+LS6vfbNAPydrPpTD/7XXHT2fyE/hjQfr69TM2i9rJn9Dm64K/DB7hM7Pj+d/HbISP/pb393/wZ4S/r6Pguv34qP0hPvAerxdgtNvvwd9NBtv93LZuO641QdlxvF8NaWbb8dPK/muBR3iVv8Ta314AXtb5G9T56wvi78Ex8G57/JvgLyfdn4E7Vf7Mx59n8QiWZ+C4HnTET8Nf5Ax8wh/8Kc7AUT3orPMncAC6wl/cDLwr8mc//hz+XKQTMCIGQL39VAH8NaXx5/xrAyDx51n8BDJT+9oASPx5Fj+B3HS91YTHPH8d/GVQhHm8BY7mr4D48wyGYPNLwLPa+HP4y2IJ2NbK36zOX1cif3ffSme0/aPh+OmU/TfsVQF3m+0fDcdPF87fzdsg0wh/8CepeUofBKDe/vGEv1L2IDvx51YfoYw9yAB/8Of0uqE3jvhz+FM8Bznhz+YjlLIJXs21f4S/zHVvEzxa4892/HkF/I33nFjG2j9mEL8a/RMom7+bTWE2W+0fzcf/SgfAZag14YEs/MGfcIH+1opstM7fBX+5lQGHFxc/jH818Hdziza/d/hZffy5yCO4ssxYvXTKCfzVzt+9g5DmtQnYt+bjp+FP/iCkeyN6WKL3mP3481r4uwfg9tLxe3T8bwH87XXw5/pERXlvOX4a/jIF8EzZdT8b/iTGvwb+dAE8DY9/K/y5TG+ln6mzh8uIP8/gJ1A9gL5i/gb4U5+Cx0D8OfwpAjjAH/wlqAMeqfrNEH9eK3+JCtED/Kk9AkdxT9eAjf34c/hzmZgRJvjLM37amfYD+oT517G97zLIHm/hzyV2RI/JzFjETxcVv+oS+ZbbVL1A4a9S/m6SsifaBsNfrfzdvJaZ6DBYm78e/pyN9oBdErDV48/jP/4Cfw81hzTb4BtnfOr8EX/uzKSEtPJDYDR/k33+hnr5u1mx28VL3If97Gfip91rzZs26YvBxJ9Xzt/NQqAX3mAXwB/x02/WYW4FdV3Z89fAn7qGhO1yLuLP64s/T7wNbgTTcS/4KzL+0iWNC75XsVo+Lgf61X72M/y51zMsbk6b40d+r220n71L/LnCLqS/Wzduf1Xo6ZYCsp+Jn3bv98l/EBXi2vPnibg/WldA9jPx007hZubDxOr22ho/hck329W+Xr0k/tyV06X37jYkg+o5/LmS8jIzSE7JgD/if/UWgdNgnD/ip51lQ4z6EAh/lVcClYfA0z5/G/y5qHamR938ET8trPm2obc1yx/x5yUUYm5aEjSfFP6KnIO1mhgTf+6wZH2zsgzwB3+aX3YzyN8Jf+XMwe9PwjlknxK/6vIwJChMwsT/Ou7G/bwTnuEP/qQ0TnmvZ+DPcT9dcRkY/fEv+CtvG/JeS+0S4s/hz0l3dX6tqVgJ/F0Q5hI4fF+51k/8eRWa+0wJhD8qMZoElsDfDl2fec0+RwKjs3fhz5VqjH6jvexA/DlDoGLAT9vDH6vAN6a5X2v15vmrLn5VYwhMVWWNvfsjET8NfzYuPHZDfttf+KukFphmIbj0gfhzx4nwDXOMZLOTOfbqD/y5Gtql/lyPkRsEl9itJ/HnRjVE9fzcZFaCY+zNC/irrhr9/btf8Tbpeffx/LXq/Hn4e3sf8m0ejl33rH3Igb/o+FX4U9iHfLsrskeMgmsT4M9xRzhyFNzHh5OvBH7EnxvXKDEHnve/wHD6AH8oPv3x75n4ujMMjnsXZNTDX+U74X/qEN31ubLMcHVTkOJvUOevh7/YnXATxHg49t8TMaxHH+SUQ/w5/MXXwLwgE8F3597+dz4e2/3cRP+jPPgj/tK9Ht/1KQybbjvO87qu8zy2rvHy/wXx5ywDVQV/ru5WHfb5I/68qGrgu8og/hf+BNV6+Lv7CPCXXz26Jv6IH8x+Kwx/yL13K80Ufzv8UYwJivHn8OeKtGZZ4S/AHwQSf44MEpgDf8S/1XskQvw0OxH4Q7VWYw4HfxCop0v/jyP+940zkTxP5ST6EhI/bUJtjt4Yibu38GdEQ5MdfxJ3L07if61o3Mo7/iB+mnLMY50z/NWmJR+Lql8d/FW4EOwy4U/Cekz8qkHN51TK9At/Rqdh/XqMSCdg+LM7CGof/o7wxyCoN/yJNJ6HP1aCz87eZKIgiD+3rlZnO9zI9J2CvwK0vz8P+z2TCDD4y2Ievt4tS0/nCH/opy/54lJwOoZcIhCJ/81Hw/EOgtMm1nSU+GkQVMQP/kBQb/Il/rzUtWDCHXF/Sd74hr9StaapC3ZrVq5u+Mt5Jr6kPfv92Tr4Q5/XcshVBv0hHvZH/HkNDJ4Sy0G/rfJ3zYifrobBJmpb3JxJvjT81bQtXo/mYa7XmqjNGfHn1UG4XLdiuHxzrul67MFfpVvj9Tr+GMnlm+Nah6wTJ+DPtOZ23f9Kh+u9/7Y8nLzv/0qO29d2zD/xhPjfsiZnZyxxB/4Q/CH4Q8gRf47gD6G3+CP+EhF/juAPIUf8OYI/hIifRvCHEPyhXEX8OYI/VC1/xJ8j+EPwR/wvctayjeEPwR+qlT/ifxH8IVdpoCz8IfhDVkX8L4I/BH/wh+zxR/wlcsSvIvhDCP4Q/CEEf8gAf8T/IvhDlYr4c2SaP+J/Efwhq/wRf47gD8Ef8asI/hD8wR9yNurP8Ic0z3/hD0VpI/4cObP3z+EPRamdiJ9GihvgHv6Q2QUg8asoTiv8IbMTMPwhp9iBCP6Q5g6Y+GmkuQOBPxQ9ABJ/iYwOgPCHojVM8IecxSZs8IfiNXv4Q85gClcDf0hAHfGryOAWBP6QU/Shwh9yikVA+ENC8vCHnLFjOOLPkeYSkPhfpLkEhD8kpwb+kKYm+EOKGoj/Rc7QbTj4Q07RiQB/yClWYXrqf8gp3sek/zNyim7oiQEQaQLY8L6QUzwI6XhfSHME7HlfSPVGHGtApLoLJgUTOc06oOceHHKaJyENJyFIVAteaOQsuWG4DYKcqh8QApFTdURDIHKqd0IgEDnVW3EQiJzqvWD6siGn2hkBApFubxgIRE61PyUEIhmNEwQiiyENEIicalAmGUnIqXbJJ6UQOdWcEAhETjUpCQKR6hAIgUh3CAx+4f0hzbxgCESqQyAEIqeVVvN3yxgIRJGaGwhEztDtuH8RuPIGkVopBgKR9iQMgShWrYdA5Cy6YiAQufc7ZUEgymsjQg9zFLsR2SAQqRLYQSDS1AiByDaBdPFFurMwBCIIRDXvhU/eIYJABIEIQSCCQITc2+fCB68QQSCCQIScij8QApEygRvRhggCEQQi5BR6dnwlkFeIVAnEG4NUCaRvDHIqSTb/76XPMhCpjoFMwkh1DPSEuyJVAhkCUTSBMT18e94fUiWQjTCK1jrhTkVGCex4e0iTQM/LQ6oEkiyMJLQ8JZBAOaRKINtgpEogACIpAj0AInMEsglBTjFPZMKRhTQJbHhpSJNALggjVQJJsEHCBPY4UpEZApmBkbiGnoM4ZINA7qYjTQInqtBIk0CuJKFUBDafcUNzCoIUCewpwaB0Gv9EYM8CECmGa8IfSqz5d63MO+ZflFzrRwfDE/tf9Mo0fPzSp78x/aK3dsPnv0fB6eD8Db25FFyPf8rS/baz+EPvT8Xtuu/7ugAfQpr6H21fBg3hfxtSAAAAAElFTkSuQmCC\" alt=\"Model Context Protocol\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Lock down Docker networks</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run containers as non-root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Membership, not just an account</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">An identity for the agent, not a key</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">6</span><span class=\"skillTracker__skill\" data-state=\"current\">A tool server that checks who is asking</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Where the agent can go, not just what it can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/\"><span class=\"skillTracker__dot\" data-state=\"locked\">8</span><span class=\"skillTracker__skill\" data-state=\"locked\">Move the daemon off root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/\"><span class=\"skillTracker__dot\" data-state=\"locked\">9</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run the model's own code without trusting it</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">A scope per tool, and a refusal clients can act on</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>La <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/\">Parte 5</a> le dio a un agente su propia\nidentidad en el gateway: un token emitido por authentik con client credentials, con un scope, y\nque expira a los cinco minutos. Terminó nombrando lo que no había cubierto — el servidor de\nherramientas MCP de la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/\">parte 4 de orquestación de agentes</a> sigue fiándose de\ncualquier cosa que alcance su puerto, <code>issue_refund</code> incluido.</p>\n<p>Aquel post fue explícito sobre el límite de lo que había construido:</p>\n<blockquote>\n<p><strong>The MCP server still trusts everyone.</strong> Scoping happens in the <em>client</em>. Anything that can\nreach <code>127.0.0.1:8770</code> can call <code>issue_refund</code> directly, agent or not.</p>\n</blockquote>\n<p>Repartir la caja de herramientas por rol impidió que un <em>agente</em> alcanzara una herramienta que\nno le tocaba. No hizo nada contra un <code>curl</code>. Esta parte cierra eso.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>La misma máquina que las Partes 3 a 5: una WEC Instance, 8 vCPU AMD EPYC 7601, 15 GB RAM,\nUbuntu 22.04, Python 3.10. authentik <code>2026.8.1</code> en el puerto host <code>9100</code>. <code>fastmcp</code> 4.0.2,\n<code>mcp</code> 2.1.1, <code>langchain</code> 1.4.0. El servidor MCP es <code>office_tools.py</code> de la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/\">parte 3 de orquestación de agentes</a>, sin más\ncambios que las líneas que se muestran aquí, todavía en <code>127.0.0.1:8770</code>.</p><p>HTTP plano sobre una LAN privada, como en toda esta serie.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"un-cliente-o-dos\">Un cliente o dos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/#un-cliente-o-dos\" class=\"hash-link\" aria-label=\"Enlace directo al Un cliente o dos\" title=\"Enlace directo al Un cliente o dos\" translate=\"no\">​</a></h2>\n<p>Lo obvio es reutilizar el cliente <code>agent-gateway</code> de la Parte 5 y añadirle un segundo scope. Ya\nexiste, el agente ya tiene sus credenciales, y funcionaría.</p>\n<p>No lo hagas. Si una sola credencial abre el gateway y el servidor de herramientas, entonces una\nclave de gateway filtrada también emite reembolsos, y el radio de daño de perderla se duplica.\nEl hilo que recorre los grupos de la Parte 4 y los grant types de la Parte 5 es que una\ncredencial debería hacer un solo trabajo.</p>\n<p>Así que el servidor de herramientas recibe su propio cliente, su propio scope y — porque en\nauthentik cada proveedor es su propio emisor — su propio emisor. Esto último acaba importando\nmás de lo esperado, y llegamos a ello en el Paso 4.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">El authentik de la <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/\">Parte 3</a>, con el cliente\n<code>agent-gateway</code> de la <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/\">Parte 5</a></li>\n<li class=\"\">El servidor MCP y el virtualenv de la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/\">parte 3 de orquestación de agentes</a></li>\n<li class=\"\"><code>fastmcp</code> 4.0.0 o posterior — el ayudante de client credentials del Paso 5 no existe antes</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--un-segundo-cliente-en-tres-partes\">Paso 1 — Un segundo cliente, en tres partes<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/#paso-1--un-segundo-cliente-en-tres-partes\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Un segundo cliente, en tres partes\" title=\"Enlace directo al Paso 1 — Un segundo cliente, en tres partes\" translate=\"no\">​</a></h2>\n<p>Esto es el procedimiento de la Parte 5 con otros nombres, así que aquí va deliberadamente\nescueto. Si algo no te resulta familiar,\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/\">la Parte 5 recorre las mismas tres pantallas</a>\ncon figuras.</p>\n<p><strong>El proveedor.</strong> Applications → Providers → New Provider → OAuth2/OpenID → Next. Nombre\n<code>agent-tools</code>, Client Type <strong>Confidential</strong>, Redirect URIs vacío, y en <strong>Grant Types</strong> desmarca\ntodo excepto <strong>Client credentials</strong>.</p>\n<p><strong>El scope.</strong> Customization → Property Mappings → New Property Mapping → Scope Mapping. Mapping\nName <code>mcp-invoke</code>, Scope name <code>mcp:invoke</code>, expresión <code>return {}</code>. Los dos campos de nombre son\ncosas distintas: el primero es la etiqueta de authentik, el segundo es la cadena que acaba en el\ntoken.</p>\n<p><strong>La aplicación.</strong> Applications → Applications → New Application ▾ → <strong>with Existing\nProvider…</strong>, con nombre <code>Agent Tools</code>, slug <code>agent-tools</code> escrito a mano, proveedor\n<code>agent-tools</code>, oculta del dashboard. Después edita el proveedor → Advanced protocol settings →\nScopes, y mueve <code>mcp-invoke</code> a Selected.</p>\n<p>Verifica los grant types desde la base de datos y no desde el formulario, que es\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/\">la lección de la Parte 4</a>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.providers.oauth2.models import OAuth2Provider</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for p in OAuth2Provider.objects.all():</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print(f'{p.name}: {p.grant_types}')\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-5</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Provider for Langfuse: ['authorization_code', 'implicit', 'urn:ietf:params:oauth:grant-type:device_code']</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Provider for LiteLLM: ['authorization_code', 'implicit', 'hybrid', 'refresh_token', 'client_credentials', 'password', 'urn:ietf:params:oauth:grant-type:device_code']</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">agent-gateway: ['client_credentials']</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">agent-tools: ['client_credentials']</span><br></div></code></pre></div></div>\n<p>Dos clientes recortados a un solo flujo cada uno y — visible en la misma salida — el proveedor\nde LiteLLM de la Parte 4 todavía con <code>password</code> y otros cinco que nadie eligió. La Parte 5 dijo\nque había que volver y arreglarlos. Esto es el aspecto que tiene no hacerlo.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--un-token-y-por-qué-es-un-token-distinto\">Paso 2 — Un token, y por qué es un token distinto<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/#paso-2--un-token-y-por-qu%C3%A9-es-un-token-distinto\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — Un token, y por qué es un token distinto\" title=\"Enlace directo al Paso 2 — Un token, y por qué es un token distinto\" translate=\"no\">​</a></h2>\n<p>Lee las credenciales y guárdalas junto a las del gateway, en su propio directorio:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">read</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> TID TSEC </span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-T</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.providers.oauth2.models import OAuth2Provider as P</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">p = P.objects.get(name='agent-tools')</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print(p.client_id, p.client_secret)\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> -1</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">mkdir</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> ~/mcp-auth </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-auth</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">umask</span><span class=\"token plain\"> 077</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">EOF</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">AK_TOKEN_URL=http://10.80.4.212:9100/application/o/token/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">AK_CLIENT_ID=</span><span class=\"token string variable\" style=\"color:#36acaa\">$TID</span><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">AK_CLIENT_SECRET=</span><span class=\"token string variable\" style=\"color:#36acaa\">$TSEC</span><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">cp</span><span class=\"token plain\"> ~/agent-auth/token.sh ~/mcp-auth/token.sh</span><br></div></code></pre></div></div>\n<p>El ayudante es el de la Parte 5, sin cambios — recibe un scope y devuelve un access token.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/mcp-auth/token.sh mcp:invoke </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-R</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'split(\".\")[1] | gsub(\"-\";\"+\") | gsub(\"_\";\"/\") | @base64d | fromjson'</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"iss\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://10.80.4.212:9100/application/o/agent-tools/\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"sub\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"545a532b77a0f877339a5e4312970069deeb9ded604494c510debdfdce9c970d\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"aud\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"xNxmOEyPMPVThmxgLsyvTCHa3qvnpVjQSOY0bZck\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"exp\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1789050030</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"iat\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1789049730</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"scope\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"mcp:invoke\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El token de herramientas decodificado, con un emisor terminado en agent-tools, una audiencia distinta de la del cliente del gateway, y scope mcp\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h6-tools-token-611a3b28adc48e09f38c168c63c7d1ac.png\" width=\"1108\" height=\"244\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Tres campos difieren del token del gateway y los tres cargan peso. El <strong>emisor</strong> termina en\n<code>/agent-tools/</code> y no en <code>/agent-gateway/</code>. La <strong>audiencia</strong> es otro client ID. El <strong>scope</strong> es\n<code>mcp:invoke</code>. <code>exp</code> menos <code>iat</code> son 300 segundos, igual que en la Parte 5 — recuerda ese número,\nvuelve en el Paso 5.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>Adjuntar un scope no es concederlo</div><div class=\"admonitionContent_BuS1\"><p>Si el token vuelve con <code>\"scope\": \"\"</code>, el mapping existe pero no está adjunto al proveedor. El\npropio texto de ayuda de authentik en ese panel: <em>\"Select which scopes can be used by the\nclient. The client still has to specify the scope to access the data.\"</em> Adjuntarlo lo hace\ndisponible; el cliente todavía tiene que pedirlo.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--el-verificador\">Paso 3 — El verificador<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/#paso-3--el-verificador\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — El verificador\" title=\"Enlace directo al Paso 3 ��— El verificador\" translate=\"no\">​</a></h2>\n<p>FastMCP valida JWT con <code>JWTVerifier</code>, que recibe las cuatro cosas que authentik nos acaba de\ndecir. Haz copia de seguridad del servidor primero, que es el artefacto sobre el que se apoyan\ndos partes ya publicadas:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cp</span><span class=\"token plain\"> ~/mcp-tools/office_tools.py ~/mcp-tools/office_tools.py.bak</span><br></div></code></pre></div></div>\n<p>Su configuración va en un fichero, no en el código:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">umask</span><span class=\"token plain\"> 077</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> .env.mcp </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">MCP_JWKS_URI=http://10.80.4.212:9100/application/o/agent-tools/jwks/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">MCP_ISSUER=http://10.80.4.212:9100/application/o/agent-tools/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">MCP_AUDIENCE=&lt;el client id de agent-tools&gt;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><br></div></code></pre></div></div>\n<p>Después, dos cambios en <code>office_tools.py</code> — un import, y un verificador donde estaba el\nconstructor pelado:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/mcp-tools/office_tools.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastmcp </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> FastMCP</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> Context</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastmcp</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">server</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">auth</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">providers</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">jwt </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> JWTVerifier</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">verifier </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> JWTVerifier</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    jwks_uri</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"MCP_JWKS_URI\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    issuer</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"MCP_ISSUER\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    audience</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"MCP_AUDIENCE\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    required_scopes</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"mcp:invoke\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">mcp </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> FastMCP</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"office-tools\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> auth</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">verifier</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Ese es todo el cambio del lado del servidor. <code>JWTVerifier</code> descarga el JWKS, empareja el <code>kid</code>\nde cada token con una clave, y comprueba firma, emisor, audiencia, caducidad y scope antes de\nque se ejecute ninguna herramienta.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span><code>ssrf_safe</code> y las direcciones privadas</div><div class=\"admonitionContent_BuS1\"><p><code>JWTVerifier</code> acepta un argumento <code>ssrf_safe</code> que por defecto es <code>False</code>, y activarlo suena a\nbuena idea sin matices. En un montaje como este rompe el verificador. La protección SSRF de\nFastMCP rechaza por IP, y su propio texto de error es <em>\"Private, loopback, link-local, and\nreserved IPs are not allowed\"</em> — la comprobación cubre explícitamente <code>10.x</code>, <code>172.16-31.x</code>,\n<code>192.168.x</code> y <code>127.x</code>. Nuestro JWKS está en <code>10.80.4.212</code>, así que la petición no sale del\nproceso.</p><p>Si quieres la protección activa y tu proveedor de identidad es genuinamente interno, el módulo\nlee <code>FASTMCP_SSRF_TRUST_PROXY</code>, que se salta la resolución DNS y la lista de bloqueo por\ncompleto.</p></div></div>\n<p>Reinicia con el nuevo entorno cargado:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">pkill</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-f</span><span class=\"token plain\"> office_tools.py</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-a</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">source</span><span class=\"token plain\"> .env.mcp </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> +a </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">nohup</span><span class=\"token plain\"> ./.venv/bin/python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-u</span><span class=\"token plain\"> office_tools.py </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> server.log </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">4</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-8</span><span class=\"token plain\"> server.log</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El servidor reiniciando, con un log de arranque que informa del transporte y el puerto y no menciona la autenticación\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h6-server-auth-startup-e2aacc2f7033430c7a21caae3e07b195.png\" width=\"856\" height=\"326\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Lee ese log de arranque con atención, por lo que le falta. Informa del transporte, el modo y el\npuerto, y no dice absolutamente nada sobre autenticación. Un servidor con verificador y uno sin\nél escriben las mismas líneas. No hay ningún mensaje de \"auth activada\" que buscar, así que la\núnica forma de saber si el verificador ha entrado es hacer una petición — que es el paso\nsiguiente.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--tres-peticiones\">Paso 4 — Tres peticiones<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/#paso-4--tres-peticiones\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — Tres peticiones\" title=\"Enlace directo al Paso 4 — Tres peticiones\" translate=\"no\">​</a></h2>\n<p>La misma forma que los tres códigos de estado de la Parte 5, porque se lee bien y porque la del\nmedio es la interesante:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">TOOLS</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/mcp-auth/token.sh mcp:invoke</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">GW</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/agent-auth/token.sh gateway:invoke</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function-name function\" style=\"color:#d73a49\">req</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$1</span><span class=\"token string\" style=\"color:#e3116c\">  HTTP %{http_code}</span><span class=\"token string entity\" style=\"color:#36acaa\">\\n</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST http://127.0.0.1:8770/mcp </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Accept: application/json, text/event-stream'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token variable\" style=\"color:#36acaa\">${2</span><span class=\"token variable operator\" style=\"color:#393A34\">:+</span><span class=\"token variable\" style=\"color:#36acaa\">-H \"Authorization</span><span class=\"token variable operator\" style=\"color:#393A34\">:</span><span class=\"token variable\" style=\"color:#36acaa\"> Bearer $2\"}</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">req </span><span class=\"token string\" style=\"color:#e3116c\">\"no token     \"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">req </span><span class=\"token string\" style=\"color:#e3116c\">\"gateway token\"</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$GW</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">req </span><span class=\"token string\" style=\"color:#e3116c\">\"tools token  \"</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$TOOLS</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">no token       HTTP 401</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">gateway token  HTTP 401</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">tools token    HTTP 200</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Tres peticiones al servidor MCP que imprimen no token 401, gateway token 401 y tools token 200\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h6-three-way-5268dcb96fa1731e359bf14df5c1b0e8.png\" width=\"633\" height=\"284\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>La primera línea es el hueco que la parte 4 de orquestación de agentes admitió, ahora cerrado.\nAntes de este cambio, esa petición devolvía el catálogo completo de cinco herramientas a quien\nlo pidiera — y con el token correcto, sigue haciéndolo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST http://127.0.0.1:8770/mcp </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$TOOLS</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Accept: application/json, text/event-stream'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tr</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'\\r'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sed</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-n</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'s/^data: //p'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token string\" style=\"color:#e3116c\">'.result.tools | length, (.[].name)'</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El listado de herramientas devuelto a quien llama autorizado: cinco herramientas, find_customer, list_availability, book_slot, open_invoices e issue_refund\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h6-tools-list-authorised-e04041592339f5df452a50d4fc2cd30d.png\" width=\"665\" height=\"206\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Las mismas cinco herramientas que antes. La autenticación cambió quién puede preguntar, no lo\nque hace el servidor.</p>\n<p>La segunda línea es en la que conviene detenerse. Ese es un <strong>token de authentik válido, sin\ncaducar y correctamente firmado</strong> que funciona perfectamente contra el gateway, y el servidor de\nherramientas lo rechaza. Pregúntale al servidor por qué — enviando la petición otra vez primero,\npara que la respuesta quede al final del log y no enterrada bajo lo que haya llegado desde\nentonces:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">GW</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/agent-auth/token.sh gateway:invoke</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'gateway token: HTTP %{http_code}\\n'</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST http://127.0.0.1:8770/mcp </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$GW</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Accept: application/json, text/event-stream'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-12</span><span class=\"token plain\"> ~/mcp-tools/server.log</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING  Bearer token rejected for client aObZ8KvKsKVdQ4MuSsTINp0f65Puz5fzXUUBC6iQ</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">         : issuer mismatch (got 'http://10.80.4.212:9100/application/o/agent-gateway/',</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                       expected 'http://10.80.4.212:9100/application/o/agent-tools/')</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">INFO     Auth error returned: invalid_token (status=401)</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El log del servidor nombrando al cliente rechazado e imprimiendo el emisor recibido y el esperado, seguido de una respuesta genérica invalid_token\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h6-issuer-mismatch-log-67e89b1720edb880e97f3f7258d47aac.png\" width=\"690\" height=\"376\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Falló por <code>iss</code>, no por el scope.</strong> Ahí está el proveedor aparte dando su fruto. En authentik\ncada proveedor es su propio emisor, así que un token del cliente equivocado se rechaza en la\nprimera puerta, antes de mirar la audiencia o el scope. Si hubiéramos tomado el camino fácil y\nañadido un segundo scope al cliente del gateway, la misma petición habría llegado varias\ncomprobaciones más allá antes de que <code>required_scopes</code> la echara. Mismo resultado, más\nsuperficie.</p>\n<p>Otras dos cosas en ese log. Nombra al cliente infractor por su ID, e imprime tanto el emisor que\nrecibió como el que esperaba — así que depurar es un <code>tail</code>, no una adivinanza. Y quien llama\nrecibe solo <code>invalid_token</code>, que es lo correcto: decirle a un cliente <em>qué</em> emisor esperabas es\ndecirle a un atacante qué falsificar.</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>No filtres este log con grep</div><div class=\"admonitionContent_BuS1\"><p><code>grep -iE 'invalid|token|401' server.log</code> devuelve la última línea y esconde la primera. Te\nquedas con <code>invalid_token (status=401)</code> y concluyes que el servidor no dice por qué. Sí lo dice,\nen la línea de arriba, repartida en cuatro líneas que tu patrón no captura. Léelo sin filtrar.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5--la-mitad-del-cliente-y-un-ayudante-que-no-puede-ayudar\">Paso 5 — La mitad del cliente, y un ayudante que no puede ayudar<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/#paso-5--la-mitad-del-cliente-y-un-ayudante-que-no-puede-ayudar\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 5 — La mitad del cliente, y un ayudante que no puede ayudar\" title=\"Enlace directo al Paso 5 — La mitad del cliente, y un ayudante que no puede ayudar\" translate=\"no\">​</a></h2>\n<p>El servidor de herramientas ya está protegido, lo que significa que todos los scripts de la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/\">parte 3</a> y la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/\">parte 4</a> están rotos. Se conectan sin token.</p>\n<p>FastMCP documenta exactamente lo que hace falta para esto: <code>ClientCredentialsOAuthProvider</code>, que\nejecuta él mismo el flujo de client credentials y — lo que importa dados nuestros tokens de 300\nsegundos — <em>\"when the token expires it is re-acquired automatically on the next request.\"</em></p>\n<p>Aquí no funciona. Pruébalo y obtienes:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">mcp.client.auth.exceptions.OAuthTokenError: Token exchange failed (404): Not Found</span><br></div></code></pre></div></div>\n<p>El log del servidor explica lo que pasó de verdad:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">GET  /.well-known/oauth-authorization-server      404</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">POST /token                                        404</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">GET  /.well-known/oauth-protected-resource         404</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">GET  /.well-known/oauth-protected-resource/mcp     404</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">GET  /.well-known/openid-configuration             404</span><br></div></code></pre></div></div>\n<p>El provider recibe la URL <strong>del servidor MCP</strong>, no un endpoint de token, porque <em>\"the token\nendpoint is discovered from the server's OAuth metadata.\"</em> El nuestro no publica ninguna — y la\npropia documentación de FastMCP lo dice, en otra página: <em>\"<code>TokenVerifier</code> focuses exclusively\non token validation without providing OAuth discovery metadata.\"</em></p>\n<p>O sea que las dos páginas son coherentes y la combinación es un callejón sin salida documentado.\nSolo que hay que leer ambas para enterarse, y el fallo no te cuenta nada de eso: cuatro 404 que\nsolo ves si estás mirando el servidor, un repliegue que hace <code>POST /token</code> contra el servidor de\nrecursos, y un error que nunca menciona el descubrimiento.</p>\n<p>El camino que señala la documentación es que el cliente obtenga su token por separado.\n<code>BearerAuth</code> recibe un token que ya tienes y lo adjunta a cada petición — vale la pena probarlo\nsuelto antes de cablearlo en cuatro scripts:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Un script de prueba que usa BearerAuth con un token del ayudante de shell e imprime las cinco herramientas descubiertas\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h6-client-bearer-works-5250c1328730dcadf4366a6843f881c0.png\" width=\"683\" height=\"427\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Eso funciona, así que se puede compartir. Un ayudante, usado por todos los scripts de la serie:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/mcp-tools/mcp_auth.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"Un único cliente MCP autenticado, compartido por todos los agentes de la serie.\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> os</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> subprocess</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastmcp </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Client</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastmcp</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">client</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">auth </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> BearerAuth</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">MCP_URL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://127.0.0.1:8770/mcp\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">TOKEN_SH </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">path</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">expanduser</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"~/mcp-auth/token.sh\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">fetch_token</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">scope</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"mcp:invoke\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"Emite un token de corta duración desde authentik con client credentials.\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    out </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> subprocess</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">TOKEN_SH</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> scope</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> capture_output</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> text</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> check</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> out</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">stdout</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">strip</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">authed_client</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> Client</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> Client</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">MCP_URL</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> auth</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">BearerAuth</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">token</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">fetch_token</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Cada agente cambia entonces en dos líneas — un import, y lo que le pasa a <code>MCPAdapter</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> mcp_auth </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> authed_client</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> MCPAdapter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">authed_client</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> adapter</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><br></div></code></pre></div></div>\n<p><code>MCPAdapter</code> acepta un <code>Client</code> ya configurado igual de bien que una URL, cosa que\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/\">la parte 3 usó para el caché</a> sin necesitarlo\npara nada más. Aquí es lo que deja entrar el token.</p>\n<p><code>BearerAuth</code> es la forma explícita. FastMCP también acepta el token como cadena pelada —\n<code>Client(url, auth=token)</code> — y añade el esquema él mismo; su documentación es específica en que\n<em>\"do not include the <code>Bearer</code> prefix\"</em> si haces eso. La clase merece el import extra aquí solo\nporque deja la intención clara en el punto de llamada.</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>Esto reintroduce el problema de la caducidad</div><div class=\"admonitionContent_BuS1\"><p><code>BearerAuth</code> guarda una cadena fija. Un token emitido al arrancar el agente está muerto 300\nsegundos después, y nada lo renueva — la renovación automática era justo lo que\n<code>ClientCredentialsOAuthProvider</code> nos habría dado. Para una ejecución corta da igual. Para un\nagente que espera a una persona, como\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/\">hace el reembolso de la parte 3</a>,\nperfectamente puede importar. O subes la vida del token en el proveedor, o lo emites por llamada\nen vez de por cliente.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-6--comprueba-que-una-herramienta-se-ejecuta-de-verdad\">Paso 6 — Comprueba que una herramienta se ejecuta de verdad<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/#paso-6--comprueba-que-una-herramienta-se-ejecuta-de-verdad\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 6 — Comprueba que una herramienta se ejecuta de verdad\" title=\"Enlace directo al Paso 6 — Comprueba que una herramienta se ejecuta de verdad\" translate=\"no\">​</a></h2>\n<p>Descubrir no es ejecutar. Un token que lista herramientas todavía podría fallar en la llamada,\nasí que comprueba lo que toca la base de datos:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">TOOLS</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/mcp-auth/token.sh mcp:invoke</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST http://127.0.0.1:8770/mcp </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$TOOLS</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Accept: application/json, text/event-stream'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"find_customer\",\"arguments\":{\"query\":\"Maria\"}}}'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tr</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'\\r'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sed</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-n</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'s/^data: //p'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.result.content[0].text // .error'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">\"[{\\\"id\\\":1,\\\"name\\\":\\\"Maria Alvarez\\\",\\\"email\\\":\\\"maria@example.com\\\"}]\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Una llamada tools/call a find_customer, autenticada con un bearer token, que devuelve la fila de Maria Alvarez desde la base de datos\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h6-tool-call-authorised-d181de9df9cf4ce219d07511a7b381d2.png\" width=\"841\" height=\"155\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Quita la cabecera <code>Authorization</code> y la misma llamada devuelve 401. Una fila real con token, nada\nsin él — eso es autenticación llegando hasta la ejecución, no solo hasta el catálogo.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes poner un verificador de JWT delante de un servidor de herramientas MCP para que rechace a\nquien llama sin identificarse y rechace tokens válidos emitidos a otro cliente — y leer el log\ndel servidor para saber cuál de emisor, audiencia, scope o caducidad hizo el rechazo.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"resolución-de-problemas--los-errores-que-dio-esta-ejecución\">Resolución de problemas — los errores que dio esta ejecución<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/#resoluci%C3%B3n-de-problemas--los-errores-que-dio-esta-ejecuci%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al Resolución de problemas — los errores que dio esta ejecución\" title=\"Enlace directo al Resolución de problemas — los errores que dio esta ejecución\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"token-exchange-failed-404-not-found\"><code>Token exchange failed (404): Not Found</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/#token-exchange-failed-404-not-found\" class=\"hash-link\" aria-label=\"Enlace directo al token-exchange-failed-404-not-found\" title=\"Enlace directo al token-exchange-failed-404-not-found\" translate=\"no\">​</a></h3>\n<p><code>ClientCredentialsOAuthProvider</code> contra un servidor con <code>JWTVerifier</code>. El descubrimiento no\nencontró nada y el cliente se replegó a adivinar. Mira el Paso 5; usa <code>BearerAuth</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"2-validation-errors--missing-required-argument\"><code>2 validation errors ... Missing required argument</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/#2-validation-errors--missing-required-argument\" class=\"hash-link\" aria-label=\"Enlace directo al 2-validation-errors--missing-required-argument\" title=\"Enlace directo al 2-validation-errors--missing-required-argument\" translate=\"no\">​</a></h3>\n<p>El token estaba bien — esto viene de dentro de la herramienta. <code>params.name</code> es el nombre de la\nherramienta, <code>params.arguments</code> son sus parámetros, y ambos anidan un campo llamado <code>name</code>.\n<code>find_customer</code> recibe <code>query</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-log-solo-dice-invalid_token\">El log solo dice <code>invalid_token</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/#el-log-solo-dice-invalid_token\" class=\"hash-link\" aria-label=\"Enlace directo al el-log-solo-dice-invalid_token\" title=\"Enlace directo al el-log-solo-dice-invalid_token\" translate=\"no\">​</a></h3>\n<p>Lo filtraste con grep. Mira el aviso del Paso 4.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"todos-los-scripts-de-las-partes-3-y-4-fallan-de-golpe\">Todos los scripts de las partes 3 y 4 fallan de golpe<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/#todos-los-scripts-de-las-partes-3-y-4-fallan-de-golpe\" class=\"hash-link\" aria-label=\"Enlace directo al Todos los scripts de las partes 3 y 4 fallan de golpe\" title=\"Enlace directo al Todos los scripts de las partes 3 y 4 fallan de golpe\" translate=\"no\">​</a></h3>\n<p>Es lo esperado. Se conectan sin token. Paso 5.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-te-dio-esto-y-qué-no\">Qué te dio esto y qué no<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/#qu%C3%A9-te-dio-esto-y-qu%C3%A9-no\" class=\"hash-link\" aria-label=\"Enlace directo al Qué te dio esto y qué no\" title=\"Enlace directo al Qué te dio esto y qué no\" translate=\"no\">​</a></h2>\n<p>Hecho: un servidor MCP que autentica a cada quien llama, una credencial que no es la del\ngateway, un rechazo que ocurre en el emisor y no en lo hondo de la comprobación de scope, y un\nlog que nombra a qué cliente se le negó el paso y por qué.</p>\n<p>Sin hacer:</p>\n<ul>\n<li class=\"\"><strong>Sin autorización por herramienta, y esto es menos de lo que prometió la parte 4 de\norquestación de agentes.</strong> Aquel post cerraba diciendo que el paso siguiente era\n<em>\"authenticate the tool call, so the server knows which agent is asking and refuses\n<code>issue_refund</code> to anything that isn't billing.\"</em> La mitad ya es cierta: la llamada está\nautenticada. La otra mitad no. <code>required_scopes</code> es una propiedad del servidor, no de una\nherramienta, así que cualquiera con <code>mcp:invoke</code> puede llamar a <code>issue_refund</code> con la misma\nfacilidad que a <code>find_customer</code>, y el servidor sigue sin poder distinguir al scheduler del\nagente de facturación. Repartir herramientas por agente, como\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/\">hizo la parte 4</a>, sigue siendo un arreglo del lado del\ncliente.</li>\n<li class=\"\"><strong>Los tokens siguen caducando a mitad de ejecución</strong>, como describe el Paso 5.</li>\n<li class=\"\"><strong>Sigue siendo HTTP plano.</strong> Un bearer token en claro es una credencial en claro.</li>\n<li class=\"\"><strong>El proveedor del propio gateway sigue con permisos de más</strong>, visible en la salida del Paso 1\ny sin arreglar desde que la Parte 5 lo señaló.</li>\n</ul>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>A tool server that checks who is asking</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-viene-después\">Qué viene después<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/#qu%C3%A9-viene-despu%C3%A9s\" class=\"hash-link\" aria-label=\"Enlace directo al Qué viene después\" title=\"Enlace directo al Qué viene después\" translate=\"no\">​</a></h2>\n<p>La autorización por herramienta es el hueco obvio y no tiene una respuesta obvia.\n<code>required_scopes</code> protege el servidor; proteger <code>issue_refund</code> de forma distinta a\n<code>find_customer</code> significa o un segundo servidor con su propio cliente, o autorización dentro del\ncuerpo de cada herramienta leyendo los claims ya verificados. Lo primero son más cajas y una\nfrontera limpia. Lo segundo es una comprobación en cada herramienta que escribas jamás, que es\nprecisamente el modo de fallo del que\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/\">avisaba la parte 4</a> para la política dentro de la\nherramienta.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"para-seguir-leyendo\">Para seguir leyendo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/#para-seguir-leyendo\" class=\"hash-link\" aria-label=\"Enlace directo al Para seguir leyendo\" title=\"Enlace directo al Para seguir leyendo\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://gofastmcp.com/servers/auth/token-verification\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">FastMCP — verificación de tokens</a></li>\n<li class=\"\"><a href=\"https://gofastmcp.com/clients/auth/client-credentials\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">FastMCP — autenticación de cliente máquina a máquina</a></li>\n<li class=\"\"><a href=\"https://gofastmcp.com/clients/auth/bearer\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">FastMCP — autenticación con bearer token</a></li>\n<li class=\"\"><a href=\"https://docs.goauthentik.io/docs/providers/oauth2/client_credentials\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">authentik — client credentials</a></li>\n<li class=\"\"><a href=\"https://datatracker.ietf.org/doc/html/rfc9728\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">RFC 9728 — OAuth 2.0 Protected Resource Metadata</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/",
            "title": "El token válido equivocado: autenticar un servidor de herramientas MCP con authentik",
            "summary": "La parte 4 de orquestación de agentes recortó las herramientas en el cliente y admitió que el servidor seguía fiándose de quien alcanzara el puerto. Esto pone un verificador de JWT delante, con su propio cliente en authentik, de modo que el servidor de herramientas rechaza a quien llama sin identificarse — y rechaza también el token válido del gateway, porque un proveedor aparte significa un emisor aparte. Incluye el ayudante de client credentials que la documentación describe y que aquí no puede funcionar, y los cuatro 404 silenciosos que produce en lugar de decirlo.",
            "date_modified": "2026-09-10T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "seguridad",
                "oauth2",
                "oidc",
                "authentik",
                "identidad",
                "mcp",
                "agentes",
                "autoalojamiento",
                "endurecimiento"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__authentik\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABCoAAACwCAYAAADJ54/8AAAliElEQVR4nO3dS24bSbvm8acSnksH6LlYG0jxW4HT8wTMGp2h6BWYnjdgegVFr8DUCj4ayMGZFbWCohLoYeNQsx400NQGsnqQLy1a1oWZzIjIy/8HCC67xIjXppiXJ+Py2z///CO4V6TxWNJ54DLws12U5ZvQRQAAAAAAHvxGUNEcCyMOv0aSLkLVg0ruJG0lrSVtJG2iLN+GKwcAAAAAhomg4kRFGk8k7b/OQtaCxt1JWklaR1m+ClsKAAAAAAwDQUUNNnJiJsKJIbmXtJS0YKQFAAAAALhDUFFBkcaJpLmkt2ErQWDfVQYW69CFAAAAAEDfEFQcgYACz7iRNGWEBQAAAAA0h6DiBUUan6sc7v8+bCVoua+S5lGW70IXAgAAAABdR1DxjCKNp5IWYg0KHOdO5eiKdehCAAAAAKDLCCoeYRQFTvQ1yvJZ6CIAAAAAoKsIKg7Ybh5LSZdhK0HH3UiaMBUEAAAAAKojqDAWUqzFVA8041blVJBN6EIAAAAAoEui0AW0ga1HsRYhBZpzKWltARgAAAAA4EiDH1FhIcW30HWgt+4lJYysAAAAAIDjDDqoYLoHPCGsAAAAAIAjDXbqByEFPDqTtLQdZQAAAAAALxhkUHGwBSkhBXy5lLQKXQQAAAAAtN0ggwqxBSnCeFuk8Tx0EQAAAADQZoNbo6JI45mkP0PXgUF7F2X5OnQRAAAAANBGgwoqijQeSdqIKR8I607SOMryXehCAAAAAKBthjb1YyFCCoR3IWkWuggAAAAAaKPBjKgo0jiR9FfoOoADv0dZvg1dBAAAAAC0yZBGVCxDFwA8Mg9dAAAAAAC0zSCCiiKNpyqH2wNtcmXrpgAAAAAAzCCCCrEeANprHroAAAAAAGiT3q9RwdoU6ADWqgAAAAAAM4QRFdPQBQCvmIYuAAAAAADaotdBRZHG55KuQtcBvGIaugAAAAAAaIs3oQtwbBK6AOAIF0Uaj6Ms34QuBAAAAAjNHjiPH/3xJsrynfdiEARBBdAOU7HoKwAAAAbK1hacSkr0zI6NRRrfSVpJWrDGW7/1ejHNIo13ks5C1wEc4TbK8nHoIgAAAACfijQeS1pIelvxpdeS5gQW/dTboILdPtBB/8FwNgB9V6TxWtUvRl9zE2V50nCbAADHijSeSfrzhCbuJU2jLF81UlBNRRrPJX1uut0oy39rus2u6PPUj3HoAoCKxpLWgWsAEFCRxlNJo6bbjbJ83nSbAAC37MFr4qDpZRtGIRRpvNTpGx+cSfp3kcYfoixfnlwUWoOgAmiPRAQVwNBN1fxoA0maO2gTAOBWIgdP6VVeb24dtHu0Io0XanZ3xm9FGouwoj/6vD3pKHQBQEXj0AUAAAAALhVpPJH00UHT32y9C/RAn4MKF0+kAJfOQxcAAAAAuGLbji4dduGybXjU56AC6Jpx6AIAAAAAh2Zyuyvjpa33hI7rZVDBkB90FFvpAgAAoM9mPekDjvUyqBBD6AEAAACgNWxtCh8P5i55cN19fQ0qAAAAAADtMfbYV+KxLzhAUAEAAAAAcC3x2NfYY19wgKACAAAAANAno9AF4DQEFQAAAAAAoDUIKgAAAAAAQGsQVAAAAAAAgNZ4E7oANOpa0vaI7/tc4/XPvebLM38+knRV8TWHEklvj/g+AAAAAO23lr/r+7WnfuAIQUW/LKMsX7/2TUUaPxc6PPv6514TZfn8me9P9ExQ8dxrHr1+LoIKAAAAoC82Pe0LDjD1AwAAAADg2rqnfcEBggoAAAAAgFNRlu9UTjV37dr6QocRVAAAAAAAfJj3pA84RlABAAAAAHAuyvKtjltYv66v1gc6jqACAAAAAOCFLax/66DpWzGaojcIKgAAAAAAPiVqNqy4lZSwNkV/EFQAAAAAALyxQCFRM2EFIUUPEVQAAAAAALyKsnwXZflY0tcTmvkqQopeIqgAAAAAAAQRZflM0u+qtnXpd0nvoiyfEVL005vQBQAAAAAAhst26pgWaTyTNJE0Ujk15NDGvtbs7NF/BBUAAAAAgOBsdMQycBloAaZ+AAAAAACA1iCoAAAAAAAArUFQAQAAAAAAWoM1KnCsd6ELwHAVaZy89P+jLF/7qQTAkBRpfC5p/Nz/59jj12vvxx7vS7e98j7voizfeCsGQDAEFTgKJ334UKTxWOUKz2OVqz2/PfJ1+/+8EStCA6ihSOORHo4/Yx1x/LFjz73smKOHY8+u+QqHw84Fh18jSRcVXr//zztJW5Xvy1bShuuZdjn43CUq3+vLI14jPby3a5WfubWL+gCEQ1ABIKgijacqL1Amks5ObO6tDm4uijS+k7SStOQJDIDH7CZpImmqI26QnnGmX4893yWtoixfnlTgQNgT9Il9JTr9XLB3YV+H741Uhtprle/RpqG+cCT73M1Uvt9HB1CPHL63n4s0vtfD+X59ao0AwnMaVDyRiMt+PTwB3UjaqUy7OWEAA2AXKXM1E0685ELSR0kfLbRYqLyI2R3zYpty8peDur5EWT530O4PRRr/46DZmyjLEwftHqVI47mkzw6afufywrZI47WOHB3ksIaqPw9B32vX7LM9k/TeURfvJb0v0nih8rizYJTFryyonsjd+/CcfbD0+SDQXvR5FJ6r41CU5b9VqGGi8nPn4nh4JulK0pW9p/O2BoUOz2VV/HUw8ugoVd7rY/TxOqVP7D5+LbfX6Yf+iLJ8dfgHjQcVdhDafx3zF9sfrN7rIRFdqucnDGCI7OZgrjA3bReS/pQ03988cOMADEuAY9CZyhuSWZHG8yjLF576bS0bPTGzL18XwC85DLRvVIbZy7Al9Yt97haqP2qpqgtJ3ywQmD2++QHwMnuguJa/Y/SHpz6njez6UaTxqEjjRZHGO0n/Vplo1v2Lnak8Yfx3kcZL+4cC0GF2jFiqHJ0Q9MmyHm4ctkUazwLXAsCDIo3H9kQ51DHoTNKfRRpv7CnV4BRpfG43jluVx+A2hBSPvVV5g7u10R44gZ371yo/d75CikMXkv5dpPGa+wngOBYmr+TvGP3puXD4pBEVB8O3r05p5wX7IVxfVQ7h2jnqZ1CaHroFvMTCgLnad1G6v3GYSpoy7QzoH7vgmqt8ANIGl5L+LtL405BGV7T4PPCcwyfyU9Y8qK4l0xv23kraFGk8ZXQF8Dw7Z67lL1i8fulcWHtEhR2ANnIXUhz6qPLpZ+KhLwANOHiS8qfafXG6v3GYhS4EQHPsmmGj9oQUh/60UWa9ZiNZNmr/eeA5Fyrn8q/sAh6vOHjP2xJS7J2pHF0xD10I0GIL+Q0ppi99Q+Wg4tEByOdJ50zlyWLhsU8ANdhaNRuFn+ZRxZ9cjAL9YNcKf6n+jgI+XNmQ9PPQhbhgN4R/K8yQ/6a9V/nAbBK6kDazEYprtfs9/zyEkBCoyj4XPgYgSNLtayGFVDGosANQ6JPOxz6f2IGus4vTf6ubT8/eS1oPdQ450HU2kmujdo6ieMpblcec89CFNMXWolirfU/UT7V/Ir/s0/vVBHvPl5K+qRvn/ivCCuCBXbt7CylUbkP9qqODioMDUBvsT+zj0IUAeGDHia5fnF6K4wvQOfaZ3ajdT3Ofcqly4bLOO3gPujSarqor9SxcOsXB7gC+bnKaQlgB6MdABF/X7neSkmPXnTwqqPA8FORY3Ey0WJTl6yjLf3vqK3RtcKOlx4m6zlReeI3DlgHgSGOVIz678DT3KW+7PrXVrsfWavd0m6ZcqpwKMg5dSAts1L1wcO+KNSswZBZS+BqIcC9pUmVzjFeDipbffJyJsAIIruXHibrOVC4AB6D9uhpQHPrY1UXDD9Ym6MP7cCyuQUtdf88/d/VzB5zCjl0LT93dqxxJsanyoheDCkv3237zwYkCCKinIQUAhNC59Q8Onsh1/Ya1Dq5B+2EZugDAp4MRcL6O29OqIYX0QlBhJ54mF6O6kfRV0peDr+8q56qcan+iOG+gLQBH6kiYCQBdcSFpFrqIY9nFblvWLwuFsKL7LpgCgqGw++WV/IUUH6IsX9V54Zun/rDBoSDfJS1fK84W4plJmqr+P9r+RJHUfD2AChyEmQAAaVak8aLKPN4QDp7I4SCsiLJ8G7oY1NKJzx1wCgsp1vK3ltCXKMuXdV/83IiKpU5LWa4l/R5l+eSYBCXK8m2U5TNJI5WjLuq6FMO3AOc8z2sDgCE5U8tHVRxc7A5xusdzziStGN3bWWcqH5gCfbaWv8Vvr6Msn5/SwC9BhQ19qvsXuJP0LsryaZ1EOcrynQUWf6hcdKOO9+JAAzhjF2FLcYEKAK5MQxfwirU4BzzlUoT4XTYLXQDgiq0p5zOkmJ7ayE9Bhd2AzGq2dS1pHGX5+rSSJBuFkah+WMGcecCdubq7FRkAdMFFkcaT0EU85cQHWkNwVaTxLHQRqOWCtUbQR54Xvr9VQ6Hf4xEVC9VLyK9tFMXu5IqMrQyaqH5YAaBhtgYM61IAgHuT0AU8ZueAz4G6v1e5MPsXlSNv36kcxftblOW/SfqP/Z9J+qRyKvFtoFrntv4aumcaugCgSbamnM+QImkqE/ixmKYdUOv8JRoZ2vGUKMs3dlJcq16A8n8l/a8ma2q5XegCGrRVeUHy2Ej+FoDBrxahCzhwL2nzxJ+/9VwHgO67VXkO3divo4OvUOecJFC/TzqY9ufTncrV6ZevbW1nF8Zr++3+133dE/t633B9zzlT+W+VeOqvq9r4uZuIKSDoiYPto324V4MhhfTzrh+zGq9vbGjHc04MK/6HpEXdLVEQjq0Qu3z85zbkNNTTnEGzg13I4b7fVR4HNsdMMbPjxv6L8AIhLXX87ghTublA/1Lx+7cOamiba0mrI3cmm6i83vF583RRpPGoRbtIzOTv738naX7KavF7dtG8lLS093IuP08X3xZpPG3i79AzfO6q7ZaTyM01zLWGcZwfLLsO7mxIIf0cVEwrvvZe0sTHNj4HYcXfNV6+tAPOrtmqgOGwJ1KLAF3fWb/Lqp9hCzPW0o/6p/J/wQOoyo2Knesa/xk9deXtnvmi8iHG7phvthuWhaSFrT0wl7+FJBO1YDczu2n08ZCgsYDiKfZeTu2hx1LuQ+xFkcYrrkEllVNx5h353I3l8Cb+8PrkNfaz6uLndNnEuoJoJ1trZeWpu31IsWm64Uj68aS06od/7jPlt7/8hxov3Q+/A1DfTH5XeL+X9CnK8lGU5Sfva247Ci2iLB+pPI7cNVAjgG65lfSvKMuPvll6LMryhcqbGF9rH4w99fOahYc+vqpclH3puqMoy7dRlicqzwcu10Jr/VazHuw/d7MTP3eJhve5AyqzkGItf9ftMxchhfSwmOak4uvu7KDhlZ28vtZ46Xt7SgWgnqnHvr5LGrk6xthxZKx6xxIA3XSthp742EOaRH5umsYe+niRXT+5XNvhXtKHU25k67LzQSK37+XMRvUN0Y2a+9xt5O9zl3joA2jcwVpCvkKKDy7D5X1QkVR83bzZMo4XZflMTy+y+JpFs5UAw2AjrnxNl/gUZbnzKWU2wmKmcuV4dhYC+s3FzmQ7lQ95XB8/Ro7bP8bMYdv7IcNLh328yMMN8FBHVVxHWd7onHVrayr3n7tzx+0DjbOQYi1/68l9cX3sjmx4SJXU5a4FCwNNVP0gdWk3XACqmXnq54PvkVq2mFciwgqgr24c7ky2lfvjY9A1dWxtCpejKSauhgxXYTfAidyFFVNH7baVs8X27edl7qLtAyEXDgfqWsrfz+61j7WvIlUfVrhqvoxqDhLVquaNFgL0nF2k+jjo/REqAD14mkZYAfTLnapPba3EjltO17wJPG1g5rDtD21azM9xWHExoIdlTlb/P2QPNVhrCjBFGi/lb/vla1cPAB6LVH1Y4ar5MqqzJ6FV55gP6UQBNGHmoY8PobcQJqwAeqnR6R4vmDtuf+y4/ZdMHbX7vQWjc3/heGrBxEGbbdSLz52NOAdar0jjhfxsuSw5HC31lEgV16doU/qt8iBV9WQya74MoLcmjtt3Pr/tWBZWTAOXAaAZ176uV+wY1ruQs0jjidwsyHavFh9rHU4teG+jFPvsxuODB9f9nDtuHziZPYD/6Km7WzkeLfVY9Pq3/KTOIpbO1JwCcklKCrzOLqhczo++9TG/rYqaI7UAtMu9/D+UWHvuz4eJo3a97+5RlU0tcDEFZOKgzTaZ+urIfoa+++oPaBsLKb556s75lK6nVA0qdi6KOIXdWFQNUKbNVwL0zsRx+1PH7dc1F3NfgS5bBrgRXnnuz4eJgzZv2jKK7ggzB20mDtpsi2tbYNantef+gFawh+69Dimk6kHFxkURDZhX/P6xgxqAvkkctv21DSu9P8UOxPPAZQCobxGgz02APp0p0jiRm2kfcwdtOmFTh5oOrX0tdhfCMkCfmwB9AkFZSLH21N0+pNh46u8nVYOKttqELgDoocRRu/dq+cWqj5X8AThxE+CprtoavJ4gcdDmTcvWOTvGvOkGLQTqm7sQ720Hf56Ak9guUGu5CZKfMgt5futLUDGr+P07BzUAvWHrU7g6CK7aPj/ZzEMXAKCyZcC++xRuJg7anDto07WVgzYTB22GtgpdANB3AUKKD6Gn6r2p+P2JiyJOYTdUs4ovWzVeCNAvI4dtLxy23Zgoy5e25ZOvEwKA060D9r2V2wWIfXrroM2/ijR20GznJKELcGAZsO8bufl5BVrjIKS49NTl19AhhVSOqNhW+P5zN2WcZK5qNxL3IqgAXpM4ave2Y0OkV6ELAHC02xDTPg6E7Lsx7Izm3Dh0AQ2779h5HeiihfyFFNdRls889fWiqkGFr3+go9jJ9Kriy0KsBg50zchRu2tH7bqyCl0AgKNtAve/Ddx/U0ahC+i5MxsN3BebwP1vA/cPOFWk8VLV73fr+h5l+dRTX6+KVPEAU6TxxEkl9Sw8vQYYmpGjdleO2nVlHboAAEfbhi6gJ8ahCxiAUegCGrQO3P82cP+Aa75CiltJU099HaVyUCE3+2pXVqTxTNXnpIXY4xnoopGLRru2QreNvroNXQeAo2xCF9ATo9AFDMA4dAEAcOBW5Taku9CFHIrsxr3KStUTW9AjGBsyN6/4stZviQi0iIsF4bp6w78NXQCAo+wC978O3H9TRqELGIDz0AU0aB26AAAnuZc0aVtIIT1sT7qq8JozhR9VsVL1lfgXjKYAgtqFLqCmTegCAAC9Mg5dQI/sQhcAdNi9ypEU29CFPKVOUCFJ81CjKmxBkaqLet6JtSmA0NahC6hpF7oAAEfZhi6gJ8ahCxiA89AF9MgmdAFAhyVt3rUnkn7MG68y/eNC0sxBPS8q0niqeguKzNo4nAVooyKNk9A1tMwmdAEAXtfWJ0IdVHXEKoZtG7oAALV8aHNIIT2MqJCkZcXXfva517aFFN9qvPR7lOWrZqsBAAAAho2AEIArh0HFosbrVz6mgJwQUtyrZdusAAAAYNCq7loHAE37ZvfYrfVm/x9Rlu+KNL5WtakVF5LWRRo7287khJBCkv5L0qxI4+YKQmhJ6AKAARuFLgAAAACN+Fak8daWgWidN49+P1e5o0eV+YmXchRWFGk8l/T5hCb+s6FSAAButq1tg03oAtALm9AFAABQ0cru4zehC3ks+uk35TyzRY12LiVtmlqzokjjUZHGa9UPKf53E3UAA7UNXUDLjEIXALdYbBlN4OcIANBBZyoHHYxDF/JY9MSfLVRtB5C9C0l/F2lce+vSIo3PbRTFRvXn791L+p81XwsMnsOFscaO2nVtFLqAmm5CFwAAAICT3HroYx9WjDz0dbRfggp7IjA9oc3PkrYWWIyOeYGNoJirfJL7WadtjTWR9H9OeD0AN85DF1DTKHQBAF5V5wELnkbI6d596AIAdEYif2GFl40yjvV4jQpJUpTl6yKNv6j+1Isze+3nIo1vJa1VjpLYHnzPWOUNQKJy6kgTPljtSUPtAWhOV1c5H4UuAMCrtqELACrYhC4AQDfYhhdTlffTpzzMP4aztSfreDKokKQoy+d2w3/qzcWlmgsiXvIpyvKlh36AIbiRg2ChSONxGxfreUVXAxYnijQeOZwe9JpzB23yJB742c5Bm78HPG4AQKdFWb6x+/K1BhRWPLVGxaGJ/Aw1OdV1lOWL0EUAeFUSuoAqOj46a+eo3ZGjdo8xdtDm1kGbQJdtHLQ5dtAmAAyGPehL5Gfq2KWkpYd+XvRiUGEpSqJ2hxXXUZZPQxcB9MzaUbuJo3ZdSUIXcIJN6AIAdNLWQZuJgzYBYFAsrJh66u59kcZLT3096bURFW0PKz4RUgBObB21+75Ni/QcYRK6gBYaB+z73EGbOwdtAl22ddDmxEGbADA4UZavJH3w1N1VyLDi1aBCam1Y8YHpHoAzG4dtTx223RjbT9rH+jqu7By1e+6o3WO4eD82DtoEOivK8rWDZi/atu0dAHSVrcvoM6xYeOrrJ0cFFVIZVkRZPpb01V05R7mX9C8WzgTccbzg5cxh202ahS7gRBtH7SaO2n1Rx0biAF3nYovSmYM2AWCQ7F74i6fuPtrOI14dHVT8eEGWzyT9oTB7QN9IGnVw1wCgi1xcqErlk7Wpo7YbYU/+rkLX0VKjQP2OHbW7cdQu0GUbB21OCRwBoDlRls8lXXvq7pvv6/fKQYX0Y27MSP5GV9yrXI8i+DYpwICsHLY9b/kF6zx0AadyNHxbCjeEO3HU7s5Ru0CXrR20eSZGVQBAo2y9xl6GFbWCCunHVJCZpN/l9h/nWuUoioXDPgD8au2w7Qu1NAywLUkZTfGySV/6dBjoAJ1lD6RcmLU8pAaAzrGwwtVI6McWto6bc7WDih8NZPnW/nF+VzlP5u7UNlWOoLiW9HuU5VNGUQD+2RSrJj7Pz/looUBr2AX0MnAZTXJ10po6avdJNoLDxUKaIaYwAl3x3UGbZ+rXMRYA2mIiPxtfnEla+wgrTg4qfjRUBhbzKMtHkv6lMrS40fEXgvcqT4ofVI6gmEZZvm2qPgC1rFy337Kna0uVoz36Yuuo3UvPIdPcUbsbR+0CfbBy1O77Io0njtoGgEHyvEunl7DijYtG7UnsZv97uxEZ22/3/707+J4toQTQSktJHx22vz/QBV9/xvaJfh+yBgc2cjeNZS4PO4DYSdDV32HjqF2g86IsX9qWdGcOml/acX/joO2T2RzskaQl16cAuiLK8p09SNrI/YO3/TX8yNU1vJOg4jErfn3wRysf/QI4TZTlmyKNb+Vm2P3epQKHFRZS9HFdio3Dtt8WaTzzsH7Q0mHbG4dtA32wlJuw+jCk3jhovzYLKb7Zbz8XaXwtaU5gAaALLKyYqLz3dhE0H3L6wLGxqR8AemvhoY99WHHuoa+f9Dik8LFQ5NzlsD97musyJFs7bBvog4XDtr3Ncz6WnQ++PfrjK0n/XaTxsm3rKgHAUywATuRnLS5n1/AEFQBeFGX5Um4X1dy7lLT1dSFYpPF5kcZr9TSkOOBiQbw9Zzca9lTT5bSjO56QAi+zz4jLnd1+PI1z2MerijQeF2m80cvngytJfxVpHLxeAHhNiLCi6UYJKgAcY+6pnzOVF4ILl6Mr7CZ4K+mtqz5aZOW4/cZvNJ55qtm0teP2gb6YO25/f9x33c+TrN+/dfzorbd6CCymruoCgFNZWDHz1N2lXb81hqACwKs8jqrY+yhp0/RFYJHGiY2i+Cb38/baYuWhj0YCJnt/NvIzymXloQ+g8zyMqtj7XKSxz1F10yKNt5I+12ziraRvVvO0scIAoEF2Df/BU3dXTYYVBBUAjjX33N+FHi4CF3WnFxRpPCrSeGYXpH9pGKMofrDFjVxO/zj0UeX0naPfL5uCM7UA6S+5XZNi7y7K8pWHfoC+mMnP8OELORytYMeb/fngm5pZFf/wXDVr2ZbbANDZsMLLrh8Aus+2qpvK/43+hcob4I9FGt+pHLK/1cPQ/W2U5dsijUcqt5OTyi2Q918+bnzbbiF/W6+e6eH9ule5s8b6ie8bKdz7swzQJ9BZtor8XNKfnrp8q3JnoYXK0U8rSes6q8pbaJrYl8vj4IXKf5+51b0Ive02AOzZdfxYbtf/2rsq0nhz6s5wBBUAqpipnMsbyoUepgX8GK5bpHGYajoiyvK1hTyu99R+7Ex2w+G535fcy89ONkCvRFm+sC3vfH6ez1Qe868kyY5jGz1sLby1r72xpHP7GtuX72l+ZyrPTzN7qjgnsADQBlGW70d9+Zhi+2eRxjsbzVELQQWAo0VZvinS+Ivqz+lFOHO5X6CyC3jKCdQ3VRkShFrj58K+fI0QO8WZpITjDYA2ibJ8ag/4fIQV34o0Vt2wgjUqAFQSZflc0k3gMlBRgAVR24jRFMAJbGHNWeAyuuJeZbADAK0SZflU0q2n7r7VXSSZoAJAHVP5WVgNzZqGLiAwhmADJ7LQ08cuIF03s60BAaCNEvkLK1Z1FsUnqABQmT1VmwQuAxVFWb7WcEfD3J66qBOAkj2NG+qx5BhfTpmXDQCu2YObRH7CijNJ66phBUEFgFrsptfXVkdozlTDHA0zDV0A0DMT+Xsa1yXXNkUSAFrtIKzwcV1YOaxgMc32+xK6gA4Yyc+CMHjE81ZHaIBt5TrTsBbW/MQQbKBZtmVponL7YbaBLl3baBMA6IRHx3LXCyWfSVoWaXzUQsMEFS1HKv86+3ARVATieasjNMACpkTDeM+umfIBuEFY8ZMbsdAogA6yXf0S+QkrLlWOrHg1rOjr1I9d6AKAIbEnSCyu1iGeV3wO5VbcOABOeZ7n3FbXUZazFSmAzrKRp4mn7vZhxflL39TLoIIhvuioTq8bYDe+fZqqdCfpU+giHEvU35uLW0ncOAAeHIQV38NWEgTTPQD0gt1D+1p/7lLS8qVv6GVQAXTUJnQBp7KpSn1YYPNW0lg9eE9e0uMnoYQUgGdRlu+iLJ9I+hq6Fo8+EFIA6BPbscjXtfz7Io2Xz9biqYgQ2DYLXbMLXUAT7AD3Tt0dIXKtAd3k9jCsIKQAAoqyfCbpD3X3HHCMO0n/YgtSAH1kxzZfo4qvngsr+hxUbEMXAFS0CV1AU2zr0pG6Fxh+irJ8OrSbXHsSOlb31xm5jrJ8PLT3D2ibKMtXKs8BfZwK8lXSmGnGAPrMFiL3dV14VaTx4pcaPHUewiZ0AUBF69AFNMlufhOViWzbn6zdqnw6tghdSEg2hPmD2v9+PXYvhmADrXIwFeQPlSMQuu5O0rsoy2eEoQCGwPNi+R+LNJ7+1L+njkPYhC4AqGgTugAX7OZ/rPaOrvhiT+E3oQtpAxvuN1Z736/HblQ+3VyGLgTAr2x0xVjlYstdC0GlsuZPUZaPbLQgAAyG57Di22FY0dugwk4mXTwhYphu+/yEJsryrY2ueKf23AB/l/S7LQCKAwfvV5ufhO6fbiZRlm9DFwPgeTa6Yq5yOsgXtfe4cmi/89No6KPtAAzeTP7WMvsRVvQ2qDDr0AUAR1qHLsCHKMvXLQgsvqu8wZ1wg/uyKMtXUZaPVE4HaUvAdCPpD55uAt2zDyxaeFw59F0Px5hFnx8iAMAxAiy8/q1I4/EbT52FspL0PnQRwBGWoQvwyW4wkyKNRypT2omkC4dd3qk8HiwqhhNblU//mrZ20KYzNq1i6fH9eqzu+9dFS3Xs56OGpZr/O24bbq8uF8eLrYM2g3t0XJnY19sApdyr/HlcSVr1OJhYqp/Hlq36/blbO2p366jdKvr2vq0D9u1clOW7Io0TldeBPiS//fPPP5768q9I43NJ/y90HaeIsvy30DW0nX1o/gpdxwnu7OnSoBVpPFZ5oZqomYvVG9nFJ+tPNO/g5iJROf+8yeDiTuWaLWuV79+2wbYBtJRdtyUqjyn7X88a7mZ/fNlIWjMyCwDaqddBhSTZvqxXoeuoi6DidT0IKr6wTsKvLLgYqbxQlcqL1udsJO1UJukbggn/7AZjrPI9G9kfJ0e8dG2/bvXw/u0aKwxAp51wbNmoPC9IdpwhlACA7hhCUJGowzexBBWv6/p7rHJBx23oIgAAAACgDfq+mOY+Pfe18AdQ1TUhBQAAAAA86H1QYRahCwCeMQ9dAAAAAAC0ySCCCltZugt7dmNYGE0BAAAAAI8MIqgw09AFAI/MQxcAAAAAAG0zmKDC1qr4HroOwHxhNAUAAAAA/GowQYWZSboPXQQG706smwIAAAAATxpUUGFPsOeBywCmUZbvQhcBAAAAAG00qKBCkqIsX4gpIAjni01DAgAAAAA8YXBBhZlKug1dBAbnJsryeegiAAAAAKDNBhlU2LD7qVivAv7cSpqELgIAAAAA2m6QQYUkRVm+kZSIsALu3Yt1KQAAAADgKIMNKqQfYcUscBnot3tJif2sAQAAAABeMeigQpKiLF9K+iBGVqB5hBQAAAAAUNHggwrpR1iRiLACzbkVIQUAAAAAVEZQYQ7WrGA3EJzqRoQUAAAAAFALQcWBg7Die9hK0GFfoyxPWDgTAAAAAOohqHgkyvJdlOUTsW4FqrmT9C7K8lnoQgAAAACgywgqnmHrVozE6Aq87qukcZTl69CFAAAAAEDXvQldQJvZ8P1JkcaJpLmktyHrQevcSJpGWb4NXQgAAAAA9AUjKo4QZfk6yvJE0juVN6cYtu8qp3kkhBQAAAAA0CxGVFRgQ/uTIo3HkmaSJpLOwlUEj+4lLSUtCCcAAAAAwB2Cihpsd5CpJBVpPFEZWCSSLgKVBDfuJK0kraMsX4UtBQAAAACGgaDiRHYDu5IkG2lx+DUS4UVX3EnaSlpL2kjaMHICAAAAAPwjqGiQjbTYPPX/LMQ491fNoGxUrh9S1c7eMwAAAABAS/x/ByfmJv7i4p4AAAAASUVORK5CYII=\" alt=\"authentik\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/litellm-wordmark-86d6f0432720b27534e05bc579dfc29c.png\" alt=\"LiteLLM\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Lock down Docker networks</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run containers as non-root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Membership, not just an account</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">5</span><span class=\"skillTracker__skill\" data-state=\"current\">An identity for the agent, not a key</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">A tool server that checks who is asking</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Where the agent can go, not just what it can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/\"><span class=\"skillTracker__dot\" data-state=\"locked\">8</span><span class=\"skillTracker__skill\" data-state=\"locked\">Move the daemon off root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/\"><span class=\"skillTracker__dot\" data-state=\"locked\">9</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run the model's own code without trusting it</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">A scope per tool, and a refusal clients can act on</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>La <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/\">Parte 4</a> puso control de acceso por\ngrupos sobre la interfaz de administración del gateway y después, al final, llamó a la API\ndesde un shell sin cuenta, sin sesión y sin grupo. Respondió con normalidad. La conclusión fue\nque el SSO protege un plano de control y que el plano de datos autentica a quien llama por\nmáquina con claves — cosa que tiene que hacer, porque un agente que corre a las 3 de la\nmañana no puede completar un login en un navegador.</p>\n<p>Eso era cierto y era también un punto y aparte, no una respuesta. \"Los clientes máquina usan\nclaves\" te deja con una credencial que nunca expira, que ningún proveedor de identidad conoce,\ny que sobrevive a la persona que la creó. La Parte 4 lo dijo sin rodeos: sacar a alguien de un\ngrupo no revoca sus claves, una clave filtrada no se ve afectada por la identidad en absoluto,\ny las claves sobreviven a las personas.</p>\n<p>Esta parte le da a la máquina una identidad en lugar de una clave.</p>\n<p>authentik le emite al agente un token con el flujo de <strong>client credentials</strong> — sin navegador,\nsin pantalla de consentimiento, sin humano. El token va firmado, lleva un scope y expira a los\ncinco minutos. El gateway lo verifica localmente contra las claves públicas de authentik y\nrechaza cualquier cosa sin el scope correcto. Al final, tres códigos de estado enseñan el\nlímite aguantando.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>Una WEC Instance: 8 vCPU AMD EPYC 7601, 15 GB RAM, Docker 29.1.3, kernel 5.15. authentik\n<code>2026.8.1</code>, la misma instancia de las Partes 3 y 4 en el puerto host <code>9100</code>. LiteLLM <code>1.96.2</code>\nde la <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/\">Parte 1 de la serie del gateway</a>,\nenlazado a <code>127.0.0.1:4000</code>, con los modelos servidos por WEC Inference. La verificación usa\n<code>PyJWT</code> 2.13.0 y <code>cryptography</code> 50.0.0, ambos ya presentes en la imagen de LiteLLM.</p><p>HTTP plano sobre una LAN privada, como en las Partes 3 y 4. Sustituye las direcciones por las\ntuyas.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"dos-flujos-dos-preguntas-distintas\">Dos flujos, dos preguntas distintas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/#dos-flujos-dos-preguntas-distintas\" class=\"hash-link\" aria-label=\"Enlace directo al Dos flujos, dos preguntas distintas\" title=\"Enlace directo al Dos flujos, dos preguntas distintas\" translate=\"no\">​</a></h2>\n<p>Las Partes 3 y 4 usaron el flujo de <strong>authorization code</strong> de principio a fin. Una persona\npulsa un botón, se le redirige a authentik, escribe una contraseña, ve una pantalla de\nconsentimiento, y vuelve con un código que la aplicación intercambia por un token. Cada paso\nde eso da por hecho un navegador y una persona sentada delante.</p>\n<p>El flujo de <strong>client credentials</strong> responde otra pregunta. No hay usuario. El cliente <em>es</em> el\nprincipal, y lo demuestra con un ID y un secreto, en una sola petición, sin redirección a\nninguna parte.</p>\n<!-- -->\n<p>La propiedad que merece atención: el gateway nunca le pregunta a authentik si un token es\nbueno. Descarga las claves públicas de authentik una vez, las cachea, y comprueba la firma\nella misma. La identidad escala sin que el proveedor de identidad se convierta en un cuello de\nbotella — ni en un punto único de fallo en cada petición.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">Un authentik funcionando, idealmente el de la <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/\">Parte 3</a></li>\n<li class=\"\">Un gateway LiteLLM en marcha, de la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/\">Parte 1 de la serie del gateway</a></li>\n<li class=\"\">Su <code>LITELLM_MASTER_KEY</code>, y acceso por shell a la máquina</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--un-cliente-con-exactamente-un-flujo\">Paso 1 — Un cliente con exactamente un flujo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/#paso-1--un-cliente-con-exactamente-un-flujo\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Un cliente con exactamente un flujo\" title=\"Enlace directo al Paso 1 — Un cliente con exactamente un flujo\" translate=\"no\">​</a></h2>\n<p><strong>Applications → Providers → New Provider → OAuth2/OpenID Provider → Next.</strong></p>\n<ul>\n<li class=\"\"><strong>Provider Name</strong> — <code>agent-gateway</code></li>\n<li class=\"\"><strong>Authorization Flow</strong> — deja <code>default-provider-authorization-explicit-consent</code>. Es\nobligatorio, y aquí es inerte: gobierna la pantalla de consentimiento que ve una persona, y\nninguna persona va a usar este cliente. Aun así tienes que elegir uno.</li>\n<li class=\"\"><strong>Client Type</strong> — <strong>Confidential</strong>. La propia descripción de authentik es la razón:\n<em>\"Confidential clients are capable of maintaining the confidentiality of their credentials\nsuch as client secrets.\"</em> Client credentials no tiene nada más que ese secreto para\ndemostrar identidad, así que un cliente público no puede usar este flujo en absoluto.</li>\n<li class=\"\"><strong>Redirect URIs</strong> — déjalo vacío. Aquí nunca se redirige nada a ninguna parte.</li>\n</ul>\n<p>Después baja hasta <strong>Grant Types</strong>, y mira con qué viene un proveedor nuevo.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El campo Grant Types en un proveedor nuevo de authentik con siete de ocho casillas marcadas: Authorization Code, Implicit, Hybrid, Refresh token, Client credentials, Password y Device-code, con solo Token exchange sin marcar\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h5-grant-types-default-a874ef714983fd8cd1c64bf2b966cb09.png\" width=\"1790\" height=\"837\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Siete de ocho, activados por defecto. Dos de ellos — <strong>Implicit</strong> y <strong>Password</strong> — no llegaron\na OAuth 2.1, que dice claramente que <em>\"some features available in OAuth 2.0, such as the\nImplicit or Resource Owner Credentials grant types, are not specified in OAuth 2.1\"</em>\n(<a href=\"https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13#section-10.1\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">§10.1</a>).\nPassword en concreto hace que el cliente maneje la contraseña real del usuario, y por eso cayó\nen desuso. Nadie te preguntó si los querías y nada te avisa de que están puestos.</p>\n<p>Desmarca todo excepto <strong>Client credentials</strong>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El mismo campo Grant Types con solo Client credentials marcado y las otras siete casillas vacías\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h5-grant-types-scoped-d5992f14733c7ab87134b9a587a05c87.png\" width=\"1900\" height=\"915\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Es el mismo reflejo de mínimo privilegio que la Parte 4 aplicó a la pertenencia a grupos, una\ncapa más abajo. Un cliente que solo va a usar un flujo debería tener permitido solo un flujo,\npara que una mala configuración en otro sitio no pueda convertirlo en un endpoint que acepta\ncontraseñas.</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>Vuelve y revisa los proveedores que ya creaste</div><div class=\"admonitionContent_BuS1\"><p>Esos valores por defecto se aplicaron también a todos los proveedores que creaste en las\nPartes 3 y 4, y nada los ha recortado desde entonces. Pregúntale a la base de datos, no a la\ninterfaz:</p><div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.providers.oauth2.models import OAuth2Provider</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for p in OAuth2Provider.objects.all():</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print(f'{p.name}: {p.grant_types}')\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-5</span><br></div></code></pre></div></div><div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Provider for Langfuse: ['authorization_code', 'implicit', 'urn:ietf:params:oauth:grant-type:device_code']</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Provider for LiteLLM: ['authorization_code', 'implicit', 'hybrid', 'refresh_token', 'client_credentials', 'password', 'urn:ietf:params:oauth:grant-type:device_code']</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">agent-gateway: ['client_credentials']</span><br></div></code></pre></div></div><p>El proveedor del gateway sigue teniendo <code>password</code> y <code>client_credentials</code> además del flujo\nque realmente usa. Ninguno hacía falta, y el flujo de contraseña en particular es una vía de\nentrada viva que nadie puso a propósito. Recorta cada proveedor a los flujos que usa.</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>Una lista de redirecciones vacía no es una regla vacía</div><div class=\"admonitionContent_BuS1\"><p>Lee el texto de ayuda del propio authentik en el campo Redirect URIs: <em>\"If no explicit\nauthorization redirect URIs are specified, the first successfully used authorization redirect\nURI will be saved.\"</em></p><p>Vacío no significa \"denegar todo\". Significa confianza en el primer uso — authentik fija la\nredirección que aparezca primero y la conserva. Eso es inofensivo para un cliente que nunca\nredirige, que es nuestro caso. No es inofensivo en un proveedor interactivo, donde significa\nque no tienes ninguna validación de redirección hasta que alguien inicia sesión y te la fija.\nLa Parte 3 avisaba de poner el modo en <code>.*</code>; esta es la versión silenciosa del mismo peligro.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--la-aplicación-a-la-que-pertenece\">Paso 2 — La aplicación a la que pertenece<a href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/#paso-2--la-aplicaci%C3%B3n-a-la-que-pertenece\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — La aplicación a la que pertenece\" title=\"Enlace directo al Paso 2 — La aplicación a la que pertenece\" translate=\"no\">​</a></h2>\n<p>Lee las credenciales e intenta obtener un token. Todavía no va a funcionar, y la razón es lo\nmás útil de este post.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">mkdir</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> ~/agent-auth</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/agent-auth</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">umask</span><span class=\"token plain\"> 077</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">EOF</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">AK_TOKEN_URL=http://10.80.4.212:9100/application/o/token/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">AK_CLIENT_ID=&lt;el client id de la página del proveedor&gt;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">AK_CLIENT_SECRET=&lt;el client secret de la página del proveedor&gt;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><br></div></code></pre></div></div>\n<p><code>umask 077</code> crea el fichero con permisos 600 en vez de hacer chmod después, así que no hay\nninguna ventana en la que el secreto sea legible por todo el mundo.</p>\n<p>Un pequeño ayudante, porque vas a pedir tokens constantemente:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> ~/agent-auth/token.sh </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">#!/usr/bin/env bash</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">set -euo pipefail</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">source \"$(dirname \"$0\")/.env\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">curl -sS -X POST \"$AK_TOKEN_URL\" \\</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  -d grant_type=client_credentials \\</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  -d client_id=\"$AK_CLIENT_ID\" \\</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  -d client_secret=\"$AK_CLIENT_SECRET\" \\</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  -d scope=\"${1:-}\" \\</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  | jq -r .access_token</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">chmod</span><span class=\"token plain\"> +x ~/agent-auth/token.sh</span><br></div></code></pre></div></div>\n<p>Pide un token:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/agent-auth</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">source</span><span class=\"token plain\"> .env</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$AK_TOKEN_URL</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">grant_type</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">client_credentials </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">client_id</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$AK_CLIENT_ID</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">client_secret</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$AK_CLIENT_SECRET</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"error\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"invalid_grant\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"error_description\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"The provided authorization grant or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"request_id\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"757c397b97a745259e288b6d4ff1553c\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>Cada cláusula de esa descripción es falsa en nuestro caso. Nada ha expirado, nada está\nrevocado, no se usó ninguna redirección, y el cliente es el dueño de esas credenciales. El log\ndel servidor registra el 400 y ninguna razón. Los Events de authentik tampoco enseñan nada\nútil.</p>\n<p>La causa real se ve en la lista de Providers, que lo marca sin disimulo:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La lista de Providers de authentik mostrando agent-gateway con un aviso que dice Provider not assigned to any application, junto a dos proveedores sanos asignados a Langfuse y LiteLLM\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h5-provider-unassigned-f6f2e4435e386fcaa6db74ac117566a4.png\" width=\"1901\" height=\"830\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Un proveedor sin aplicación no puede emitir un token.</strong> authentik autoriza contra la\n<em>aplicación</em>, así que un proveedor por su cuenta no tiene contra qué autorizar — y dice\n<code>invalid_grant</code> en lugar de decir eso.</p>\n<p>Esto no está en la documentación de client credentials de authentik, que documenta cinco formas\nde autenticarse — tres con credenciales estáticas y dos con JWT — y no dice nada sobre\nnecesitar una aplicación.</p>\n<p>Empareja una. Fíjate en la ruta: el asistente de <strong>New Application</strong> siempre crea un proveedor\n<em>nuevo</em>, así que no puede adoptar el que acabamos de construir.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El paso Configure the Application del asistente New application, con su lista de cinco pasos a la izquierda: Application, Choose a Provider, Configure Provider, Configure Bindings, Review and Submit\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h5-application-created-4cf5cd7238257dde87681d0c92e34efd.png\" width=\"1894\" height=\"913\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Su lista de pasos lo delata — <strong>Choose a Provider</strong> es el paso 2, y todas las opciones de ahí\nconstruyen uno nuevo. La flecha junto al botón es donde vive el otro camino.</p>\n<p><strong>Applications → Applications → New Application ▾ → with Existing Provider…</strong></p>\n<ul>\n<li class=\"\"><strong>Name</strong> — <code>Agent Gateway</code></li>\n<li class=\"\"><strong>Slug</strong> — <code>agent-gateway</code>, escrito a mano. El hallazgo de la Parte 4 fue que authentik\nderiva los slugs de los nombres y convirtió <code>LiteLLM</code> en <code>lite-llm</code>; el slug acaba en la URL\ndel emisor, así que merece la pena ponerlo a mano siempre.</li>\n<li class=\"\"><strong>Provider</strong> — <code>agent-gateway</code></li>\n<li class=\"\"><strong>Hidden from Application Dashboard</strong> — márcalo. Ninguna persona debería ver nunca una\ntarjeta de un cliente máquina.</li>\n</ul>\n<p>Repite la petición de token y funciona. No cambió nada salvo el emparejamiento.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--un-scope-que-significa-algo\">Paso 3 — Un scope que significa algo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/#paso-3--un-scope-que-significa-algo\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — Un scope que significa algo\" title=\"Enlace directo al Paso 3 — Un scope que significa algo\" translate=\"no\">​</a></h2>\n<p>El token que obtienes ahora lleva <code>\"scope\": \"\"</code>. Demuestra <em>quién</em> llama y no concede ninguna\nautoridad. Un gateway que lo reciba sabe que el cliente es <code>agent-gateway</code> y nada más.</p>\n<p><strong>Customization → Property Mappings → New Property Mapping → Scope Mapping.</strong></p>\n<ul>\n<li class=\"\"><strong>Mapping Name</strong> — <code>gateway-invoke</code>, la etiqueta del objeto dentro de authentik</li>\n<li class=\"\"><strong>Scope name</strong> — <code>gateway:invoke</code>, la cadena que piden los clientes y que aparece en el token</li>\n<li class=\"\"><strong>Expression</strong> — <code>return {}</code></li>\n</ul>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El formulario de Scope Mapping con Mapping Name gateway-invoke, Scope name gateway, una descripción, y la expresión return llaves vacías\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h5-scope-mapping-93c6d51e31e1b4e0e089d72f023f4714.png\" width=\"1895\" height=\"867\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Los dos campos de nombre son cosas distintas y el formulario no lo deja claro. La expresión es\nPython cuyo valor de retorno se fusiona con los claims del token; un diccionario vacío es lo\ncorrecto aquí, porque queremos el scope <em>nombrado</em> en el token, no datos extra dentro de él.</p>\n<p>Adjúntalo: <strong>Applications → Providers → <code>agent-gateway</code> → Edit → Advanced protocol settings →\nScopes</strong>, y mueve <code>gateway-invoke</code> de Available a Selected.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La lista doble de Scopes del proveedor con email, openid, profile y gateway-invoke en la columna Selected\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h5-provider-scopes-1cf3161c12ea30bfd90af1bc227bb8d1.png\" width=\"1836\" height=\"841\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Lee el texto de ayuda bajo ese panel, porque es todo el mecanismo:</p>\n<blockquote>\n<p><em>\"Select which scopes can be used by the client. The client still has to specify the scope to\naccess the data.\"</em></p>\n</blockquote>\n<p>Adjuntar un scope lo hace <em>disponible</em>. El cliente todavía tiene que pedirlo. Por eso el primer\ntoken volvió vacío aunque ya había tres scopes adjuntos — nadie los había pedido.</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>Pulsar un scope ya seleccionado lo marca para borrar</div><div class=\"admonitionContent_BuS1\"><p>La lista doble marca entradas para borrado cuando las pulsas en la columna derecha, y la única\nseñal es una línea de texto pequeño encima que dice <em>\"4 items selected. 1 item marked to\nremove.\"</em> Guardar en ese momento desvincula en silencio el scope que acabas de añadir. Lee esa\nlínea antes de guardar.</p></div></div>\n<p>Ahora pídelo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">TOKEN</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/agent-auth/token.sh gateway:invoke</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-R</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'split(\".\")[1] | gsub(\"-\";\"+\") | gsub(\"_\";\"/\") | @base64d | fromjson'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;&lt;</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$TOKEN</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"iss\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://10.80.4.212:9100/application/o/agent-gateway/\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"sub\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"2451c6a1bab3085590263f119e650448beb013336146bcfee8241dd5832c200b\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"aud\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"aObZ8KvKsKVdQ4MuSsTINp0f65Puz5fzXUUBC6iQ\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"exp\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1788901322</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"iat\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1788901022</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"acr\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"goauthentik.io/providers/oauth2/default\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"jti\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"NRDTukijwLJbW7gpPtn9zUZwzTj6d2kBSZCuFYvX\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"azp\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"aObZ8KvKsKVdQ4MuSsTINp0f65Puz5fzXUUBC6iQ\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"uid\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"neugHWoWQOQzADgbV1vKwzew2COQ1RgCXfyywgqa\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"scope\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"gateway:invoke\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El payload del JWT decodificado mostrando iss, sub, aud, exp, jti, uid y scope con el valor gateway\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h5-token-scoped-83408e96e6123963ab117fedea8d70de.png\" width=\"695\" height=\"276\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><code>exp</code> menos <code>iat</code> son 300 segundos. Ese es el argumento entero a favor de esto frente a una\nclave estática: un token pegado en un chat, en un fichero de log o en una captura no vale nada\ncinco minutos después.</p>\n<p>Ahí no hay <code>email</code> ni <code>preferred_username</code>, porque no hay usuario. <code>sub</code> es una cuenta de\nservicio que authentik creó por ti, llamada <code>ak-agent-gateway-client_credentials</code> — aparece en\n<strong>Directory → Users</strong> sin que nadie la pidiera.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--enseñarle-al-gateway-a-verificar\">Paso 4 — Enseñarle al gateway a verificar<a href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/#paso-4--ense%C3%B1arle-al-gateway-a-verificar\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — Enseñarle al gateway a verificar\" title=\"Enlace directo al Paso 4 — Enseñarle al gateway a verificar\" translate=\"no\">​</a></h2>\n<p>LiteLLM documenta la autenticación con JWT con un bloque <code>litellm_jwtauth</code>,\n<code>enforce_scope_based_access</code> y <code>scope_mappings</code>. Configúralo en una instalación estándar y no\npasa nada, porque esa misma página dice:</p>\n<blockquote>\n<p><em>\"JWT-based Auth requires a LiteLLM Enterprise license.\"</em></p>\n</blockquote>\n<p>Compruébalo antes de escribir configuración:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> llm-gateway python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from litellm.proxy.proxy_server import premium_user</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('premium_user =', premium_user)\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">premium_user = False</span><br></div></code></pre></div></div>\n<p><code>False</code> significa que todo ese camino documentado está cerrado para ti. La alternativa de\ncódigo abierto es <code>custom_auth</code>, que le pasa cada petición a una función que escribes tú — y\npara este trabajo son unas veinticinco líneas.</p>\n<div class=\"theme-admonition theme-admonition-danger admonition_xJq3 alert alert--danger\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z\"></path></svg></span>custom_auth reemplaza la autenticación por clave por completo</div><div class=\"admonitionContent_BuS1\"><p>Con <code>custom_auth</code> puesto, <em>todas</em> las peticiones pasan por tu función, incluidas las claves\nvirtuales <code>sk-</code> del resto de la serie del gateway y la propia master key. Un handler que\nsolo entienda JWT te deja fuera de tu propio gateway. El de abajo cae de vuelta a la master\nkey a propósito.</p><p>Haz copia de seguridad antes de empezar: <code>cp config.yaml config.yaml.bak</code></p></div></div>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/llm-gateway/jwt_auth.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> os</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> jwt</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastapi </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Request</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> litellm</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">proxy</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">_types </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> UserAPIKeyAuth</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">JWKS_URL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"JWT_PUBLIC_KEY_URL\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ISSUER </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"JWT_ISSUER\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">AUDIENCE </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"JWT_AUDIENCE\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">REQUIRED_SCOPE </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"gateway:invoke\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">MASTER_KEY </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"LITELLM_MASTER_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">_jwks </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> jwt</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">PyJWKClient</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">JWKS_URL</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">user_api_key_auth</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">request</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Request</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> api_key</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> UserAPIKeyAuth</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    token </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> api_key</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">removeprefix</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"Bearer \"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">strip</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># No es un JWT: cae a la master key para que las herramientas existentes sigan funcionando.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> token</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">count</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\".\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">!=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> token </span><span class=\"token operator\" style=\"color:#393A34\">==</span><span class=\"token plain\"> MASTER_KEY</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> UserAPIKeyAuth</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">api_key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">token</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">raise</span><span class=\"token plain\"> Exception</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"not a JWT and not the master key\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    signing_key </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> _jwks</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get_signing_key_from_jwt</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">token</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    claims </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> jwt</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">decode</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        token</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        signing_key</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">key</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        algorithms</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"RS256\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        issuer</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">ISSUER</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        audience</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">AUDIENCE</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> REQUIRED_SCOPE </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> claims</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"scope\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">split</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">raise</span><span class=\"token plain\"> Exception</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"token is missing scope </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">REQUIRED_SCOPE</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> UserAPIKeyAuth</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">api_key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">token</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> user_id</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">claims</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"sub\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p><code>PyJWKClient</code> hace la parte que parece difícil. Descarga el JWKS, empareja la cabecera <code>kid</code>\ndel token con la clave correcta, y la cachea — así que después de la primera petición la\nverificación es aritmética local. <code>jwt.decode</code> comprueba la firma, el emisor, la audiencia y la\nexpiración en una sola llamada, y lanza excepción si alguna falla.</p>\n<p>Tres variables de entorno, añadidas a <code>~/llm-gateway/.env</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">JWT_PUBLIC_KEY_URL</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">http://10.80.4.212:9100/application/o/agent-gateway/jwks/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">JWT_ISSUER</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">http://10.80.4.212:9100/application/o/agent-gateway/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">JWT_AUDIENCE</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">aObZ8KvKsKVdQ4MuSsTINp0f65Puz5fzXUUBC6iQ</span><br></div></code></pre></div></div>\n<p><code>JWT_ISSUER</code> y <code>JWT_AUDIENCE</code> tienen que coincidir exactamente con el <code>iss</code> y el <code>aud</code> que\nviste en el token decodificado, o se rechazan todos los tokens.</p>\n<p>Monta el módulo igual que el gateway ya monta <code>scrubber.py</code>, en <code>docker-compose.yml</code>:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> ./jwt_auth.py</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/app/jwt_auth.py</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">ro</span><br></div></code></pre></div></div>\n<p>Y en <code>config.yaml</code>, bajo <code>general_settings</code>:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">custom_auth</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> jwt_auth.user_api_key_auth</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">custom_auth_run_common_checks</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean important\" style=\"color:#36acaa\">true</span><br></div></code></pre></div></div>\n<p>Esa segunda línea importa más de lo que parece. Sin ella, LiteLLM avisa al arrancar:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El log de arranque de LiteLLM, con el aviso de custom_auth_run_common_checks entre los avisos de registro de modelos, terminando en Application startup complete\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h5-custom-auth-warning-863f445ce677a95c6cf806de7ecfc4c7.png\" width=\"942\" height=\"972\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<blockquote>\n<p><em>\"custom_auth is configured but 'custom_auth_run_common_checks' is not set. Problem: budgets,\nmodel-access allowlists, and per-model rate limits configured on your DB team/project records\nwill NOT be enforced for custom-auth requests.\"</em></p>\n</blockquote>\n<p>Léelo con calma. Añadir autenticación por token habría dejado, por defecto, a todos los\nportadores de un token exentos de los controles que ya tenías configurados. Habrías hecho el\ngateway más seguro en una dimensión y más débil en otra sin enterarte, y el único aviso es una\nlínea al arrancar.</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>Editar una configuración montada no reinicia nada</div><div class=\"admonitionContent_BuS1\"><p><code>config.yaml</code> es un bind mount. Cambiar su contenido no cambia ninguna parte de la\nespecificación de compose, así que <code>docker compose up -d</code> informa\n<code>Container llm-gateway  Running</code> y no hace nada. Tus pruebas corren entonces contra el proceso\nviejo y reproducen los resultados viejos exactamente, lo que parece que tu cambio no tuvo\nefecto.</p><p>Usa <code>docker restart llm-gateway</code>, y confírmalo viendo <em>desaparecer</em> el aviso de arriba del log.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5--el-límite-en-tres-códigos-de-estado\">Paso 5 — El límite, en tres códigos de estado<a href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/#paso-5--el-l%C3%ADmite-en-tres-c%C3%B3digos-de-estado\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 5 — El límite, en tres códigos de estado\" title=\"Enlace directo al Paso 5 — El límite, en tres códigos de estado\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">TOKEN</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/agent-auth/token.sh gateway:invoke</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">NOSCOPE</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/agent-auth/token.sh</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'no token      HTTP %{http_code}\\n'</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-mid\",\"messages\":[{\"role\":\"user\",\"content\":\"Say OK.\"}]}'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'wrong scope   HTTP %{http_code}\\n'</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$NOSCOPE</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-mid\",\"messages\":[{\"role\":\"user\",\"content\":\"Say OK.\"}]}'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'valid token   HTTP %{http_code}\\n'</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$TOKEN</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-mid\",\"messages\":[{\"role\":\"user\",\"content\":\"Say OK.\"}]}'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">no token      HTTP 401</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">wrong scope   HTTP 401</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">valid token   HTTP 429</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Tres llamadas curl imprimiendo no token HTTP 401, wrong scope HTTP 401 y valid token HTTP 429\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h5-three-status-codes-0db858ce4b80dda45f7c2626ec09529c.png\" width=\"804\" height=\"293\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Las dos primeras no llegan a ningún modelo. La segunda es la que importa: un token <strong>válido,\ncorrectamente firmado, sin expirar y del emisor correcto</strong>, rechazado porque no llevaba\n<code>gateway:invoke</code>. La identidad por sí sola no es autoridad.</p>\n<p>La tercera es la interesante, y no es un 200. Pasó la autenticación y el scope, y la respondió\nuna parte completamente distinta del gateway — la capa de presupuesto y enrutado que\nconfigura la <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/\">serie del gateway</a>, que en esta\nmáquina ya tenía un tope alcanzado. Ese es el sentido de la forma, no un fallo en ella: un 401\nsignifica <em>no sabemos quién eres, o no puedes hacer esto</em>, y cualquier otra cosa significa que\nla petición pasó la identidad por completo y ahora es decisión de otro. El trabajo de la\nautenticación termina en las dos primeras líneas.</p>\n<p>Confirma que el rechazo vino de tu comprobación y no de otra cosa:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> logs llm-gateway </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--since</span><span class=\"token plain\"> 10m </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-iE</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'missing scope|401 Unauthorized'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-5</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">user_api_key_auth(): Exception occured - token is missing scope gateway:invoke</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Exception: token is missing scope gateway:invoke</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">INFO:     \"POST /v1/chat/completions HTTP/1.1\" 401 Unauthorized</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El log del gateway mostrando la excepción token is missing scope gateway seguida de una respuesta 401 Unauthorized\" src=\"https://development-wec.wiline.com/docs/es/assets/images/h5-scope-refusal-log-aec61cc01ab970a987dee6c534e39c59.png\" width=\"939\" height=\"152\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Fíjate en dónde vive esa razón. Quien llama recibe un 401 pelado sin explicación, y está bien\n— decirle a un cliente <em>\"tu token es válido pero le falta el scope X\"</em> le dice a un atacante\nexactamente qué tiene que ir a buscar. Eso sí significa que depurar esto pasa siempre por leer\nel log del gateway, y nunca por el cuerpo de la respuesta.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes darle a un agente headless su propia identidad: un cliente confidencial restringido a un\nsolo flujo, un scope que significa algo, y un gateway que verifica la firma localmente y\nrechaza cualquier cosa sin el scope — con el rechazo demostrado desde el log en vez de supuesto\ndesde un código de estado.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"resolución-de-problemas--los-errores-que-dio-esta-ejecución\">Resolución de problemas — los errores que dio esta ejecución<a href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/#resoluci%C3%B3n-de-problemas--los-errores-que-dio-esta-ejecuci%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al Resolución de problemas — los errores que dio esta ejecución\" title=\"Enlace directo al Resolución de problemas — los errores que dio esta ejecución\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"invalid_client\"><code>invalid_client</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/#invalid_client\" class=\"hash-link\" aria-label=\"Enlace directo al invalid_client\" title=\"Enlace directo al invalid_client\" translate=\"no\">​</a></h3>\n<p>Las credenciales no autenticaron en absoluto. Antes de culpar a authentik, mira qué acabó de\nverdad en tu <code>.env</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">awk</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-F</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{printf \"%-18s %3d chars\\n\", $1, length($2)}'</span><span class=\"token plain\"> ~/agent-auth/.env</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">AK_TOKEN_URL        48 chars</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">AK_CLIENT_ID        40 chars</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">AK_CLIENT_SECRET   128 chars</span><br></div></code></pre></div></div>\n<p>El client ID tiene 40 caracteres y el secreto 128. Un valor de longitud cero significa que el\nproblema es el fichero, no el proveedor.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"invalid_grant-con-todo-aparentemente-correcto\"><code>invalid_grant</code>, con todo aparentemente correcto<a href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/#invalid_grant-con-todo-aparentemente-correcto\" class=\"hash-link\" aria-label=\"Enlace directo al invalid_grant-con-todo-aparentemente-correcto\" title=\"Enlace directo al invalid_grant-con-todo-aparentemente-correcto\" translate=\"no\">​</a></h3>\n<p>El proveedor no está emparejado con una aplicación. Mira el Paso 2 — la lista de Providers lo\nseñala, el texto del error no.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"ak_token_url-unbound-variable-y-un-secreto-corrupto\"><code>AK_TOKEN_URL: unbound variable</code>, y un secreto corrupto<a href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/#ak_token_url-unbound-variable-y-un-secreto-corrupto\" class=\"hash-link\" aria-label=\"Enlace directo al ak_token_url-unbound-variable-y-un-secreto-corrupto\" title=\"Enlace directo al ak_token_url-unbound-variable-y-un-secreto-corrupto\" translate=\"no\">​</a></h3>\n<p><code>echo 'X=1' &gt;&gt; .env</code> sobre un fichero cuya última línea no termina en salto de línea añade a\nesa línea en vez de crear una nueva:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">AK_CLIENT_SECRET=p94oDU...dgJcAK_TOKEN_URL=http://10.80.4.212:9100/application/o/token/</span><br></div></code></pre></div></div>\n<p>Dos variables destruidas con un solo comando, en silencio — el secreto ahora está mal <em>y</em> la\nvariable nueva no existe. Compruébalo con <code>cat -A</code>, que marca los finales de línea con <code>$</code>, y\nprefiere escribir el fichero entero con un solo heredoc antes que ir añadiendo.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-cambio-de-configuración-no-tuvo-efecto\">El cambio de configuración no tuvo efecto<a href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/#el-cambio-de-configuraci%C3%B3n-no-tuvo-efecto\" class=\"hash-link\" aria-label=\"Enlace directo al El cambio de configuración no tuvo efecto\" title=\"Enlace directo al El cambio de configuración no tuvo efecto\" translate=\"no\">​</a></h3>\n<p>El contenedor nunca se reinició. Mira el aviso del Paso 4.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"401-sin-ninguna-razón-en-la-respuesta\"><code>401</code> sin ninguna razón en la respuesta<a href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/#401-sin-ninguna-raz%C3%B3n-en-la-respuesta\" class=\"hash-link\" aria-label=\"Enlace directo al 401-sin-ninguna-razón-en-la-respuesta\" title=\"Enlace directo al 401-sin-ninguna-razón-en-la-respuesta\" translate=\"no\">​</a></h3>\n<p>Es a propósito. La razón está en <code>docker logs llm-gateway</code>.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-te-dio-esto-y-qué-no\">Qué te dio esto y qué no<a href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/#qu%C3%A9-te-dio-esto-y-qu%C3%A9-no\" class=\"hash-link\" aria-label=\"Enlace directo al Qué te dio esto y qué no\" title=\"Enlace directo al Qué te dio esto y qué no\" translate=\"no\">​</a></h2>\n<p>Hecho: una identidad de máquina emitida por tu proveedor de identidad en vez de acuñada a mano,\nuna credencial que expira en cinco minutos, un scope que hay que pedir y que se comprueba en\ncada llamada, revocación en un solo sitio, y alguien a quien el gateway puede nombrar en sus\nlogs como algo más que un prefijo de clave.</p>\n<p>Sin hacer:</p>\n<ul>\n<li class=\"\"><strong>El servidor MCP sigue abierto.</strong> Todo esto protege el gateway. El servidor de herramientas\nde la <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/\">Parte 4 de orquestación de agentes</a> todavía se\nfía de cualquier cosa que alcance su puerto, <code>issue_refund</code> incluido. El mismo problema, una\ncapa más arriba, y el tema de la próxima parte.</li>\n<li class=\"\"><strong>Un scope, un cliente.</strong> Un despliegue real tiene varios agentes con scopes distintos, y\n<code>scope_mappings</code> entre scopes y modelos es a dónde va eso.</li>\n<li class=\"\"><strong>La master key sigue funcionando</strong>, a propósito, como fallback en el handler. Sigue siendo\nla credencial de emergencia que describió la Parte 4, y sigue saltándose todo.</li>\n<li class=\"\"><strong>Sigue siendo HTTP plano.</strong> El token cruza la red en claro, y un token en claro es una clave\nen claro hasta que expira.</li>\n<li class=\"\"><strong>La cuenta de servicio es invisible.</strong> authentik creó\n<code>ak-agent-gateway-client_credentials</code> sin preguntar, y nada en el token dice que sea una\nmáquina y no una persona.</li>\n</ul>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>An identity for the agent, not a key</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-viene-después\">Qué viene después<a href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/#qu%C3%A9-viene-despu%C3%A9s\" class=\"hash-link\" aria-label=\"Enlace directo al Qué viene después\" title=\"Enlace directo al Qué viene después\" translate=\"no\">​</a></h2>\n<p>El servidor MCP. FastMCP trae un <code>JWTVerifier</code> que acepta un <code>jwks_uri</code>, un <code>issuer</code>, una\n<code>audience</code> y <code>required_scopes</code> — las mismas cuatro cosas configuradas aquí — así que el\nservidor de herramientas puede exigir un token con scope antes de listar una herramienta, no\ndigamos ejecutarla. Eso cierra el hueco que dejó abierto la Parte 4 de orquestación de agentes,\ndonde el recorte pasaba en el cliente y el servidor se fiaba de quien alcanzara el puerto.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"para-seguir-leyendo\">Para seguir leyendo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/#para-seguir-leyendo\" class=\"hash-link\" aria-label=\"Enlace directo al Para seguir leyendo\" title=\"Enlace directo al Para seguir leyendo\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.goauthentik.io/docs/providers/oauth2/client_credentials\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">authentik — client credentials</a></li>\n<li class=\"\"><a href=\"https://docs.goauthentik.io/docs/add-secure-apps/providers/property-mappings/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">authentik — property mappings de proveedor, y scope mappings con OAuth2</a></li>\n<li class=\"\"><a href=\"https://docs.litellm.ai/docs/proxy/custom_auth\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">LiteLLM — custom auth</a></li>\n<li class=\"\"><a href=\"https://docs.litellm.ai/docs/proxy/token_auth\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">LiteLLM — OIDC JWT-based auth</a>, el camino Enterprise</li>\n<li class=\"\"><a href=\"https://pyjwt.readthedocs.io/en/stable/usage.html#retrieve-rsa-signing-keys-from-a-jwks-endpoint\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">PyJWT — <code>PyJWKClient</code></a></li>\n<li class=\"\"><a href=\"https://datatracker.ietf.org/doc/html/rfc6749#section-4.4\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">RFC 6749 §4.4 — el flujo de client credentials</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/",
            "title": "La clave que expira: darle a un agente su propia identidad en el gateway",
            "summary": "La Parte 4 terminó con un shell, una clave estática y un modelo que respondió. Esto reemplaza esa clave por un token que emite tu proveedor de identidad, que lleva un scope, y que muere a los cinco minutos. authentik lo emite por client credentials, el gateway lo verifica en veinticinco líneas, y tres códigos de estado demuestran el límite. Incluye el requisito que authentik no documenta, y el muro de licencia con el que chocas si sigues la propia guía de LiteLLM.",
            "date_modified": "2026-09-08T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "seguridad",
                "oauth2",
                "oidc",
                "authentik",
                "identidad",
                "litellm",
                "gateway",
                "agentes",
                "endurecimiento"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__mcp\" src=\"data:image/png;base64,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\" alt=\"Model Context Protocol\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"487\" height=\"249\" fill=\"none\" viewBox=\"0 0 487 249\" class=\"tutorialHero__brand\"><path fill=\"#1C3C3C\" fill-rule=\"evenodd\" d=\"M124.273.412h239.031c68.111 0 123.52 55.603 123.52 123.948s-55.409 123.948-123.52 123.948H124.273c-68.11 0-123.52-55.603-123.52-123.948S56.164.412 124.274.412M234.018 192.55c3.001 3.156 7.443 3 11.378 2.182l.039.02c1.827-1.486-.77-3.368-3.249-5.165-1.487-1.077-2.932-2.125-3.356-3.038 1.373-1.674-2.687-5.474-5.847-8.433-1.327-1.242-2.495-2.335-3.037-3.061-2.248-2.448-3.152-5.537-4.062-8.644-.603-2.061-1.208-4.131-2.211-6.027-6.176-14.339-13.248-28.561-23.165-40.699-6.373-8.067-13.646-15.291-20.922-22.518-4.691-4.66-9.382-9.319-13.835-14.206-4.581-4.73-7.338-10.556-10.1-16.392-2.312-4.886-4.627-9.779-8.018-14.04-10.268-15.196-42.686-19.346-47.44 2.124.019.662-.195 1.09-.78 1.52-2.63 1.928-4.968 4.11-6.935 6.76-4.812 6.721-5.553 18.118.448 24.158l.025-.388c.2-3.05.388-5.9 2.8-8.087 4.637 3.994 11.67 5.416 17.047 2.435 6.483 9.3 8.548 20.543 10.621 31.823 1.726 9.398 3.457 18.821 7.751 27.171l.266.443c2.524 4.202 5.089 8.472 8.326 12.142 1.176 1.823 3.59 3.791 6 5.755 3.18 2.591 6.353 5.177 6.663 7.416.015.974.01 1.961.006 2.954-.025 5.879-.051 11.967 3.716 16.801 2.084 4.228-3.02 8.475-7.131 7.949-2.254.312-4.716-.282-7.161-.871-3.346-.807-6.659-1.607-9.36-.064-.758.82-1.846.849-2.94.877-1.297.035-2.601.069-3.373 1.422-.158.402-.528.856-.913 1.328-.846 1.036-1.762 2.159-.665 3.016q.149-.111.294-.224c1.663-1.269 3.248-2.479 5.493-1.724-.299 1.658.772 2.103 1.843 2.547q.281.114.553.239c-.011.385-.087.774-.163 1.159-.18.921-.356 1.824.358 2.62.339-.344.639-.732.939-1.12.735-.95 1.474-1.904 2.802-2.25 2.92 3.9 5.862 2.28 9.554.248 4.164-2.293 9.281-5.111 16.396-1.125-2.727-.136-5.163.195-6.994 2.455-.448.507-.838 1.091-.039 1.754 4.21-2.728 5.961-1.748 7.61-.825 1.19.666 2.326 1.302 4.294.493.465-.242.93-.493 1.396-.745 3.161-1.705 6.366-3.434 10.118-2.839-2.803.808-3.8 2.584-4.888 4.524-.539.959-1.099 1.957-1.911 2.898-.429.429-.624.936-.137 1.656 5.869-.488 8.087-1.976 11.083-3.986 1.429-.959 3.036-2.038 5.302-3.183 2.505-1.542 5.009-.556 7.436.4 2.633 1.037 5.175 2.037 7.527-.264.743-.7 1.674-.708 2.602-.717a10 10 0 0 0 1.002-.043c-.732-3.92-4.861-3.874-9.052-3.827-4.847.054-9.778.109-9.632-5.972 4.504-3.076 4.546-8.415 4.585-13.461.01-1.218.019-2.419.091-3.567 3.313 1.847 6.817 3.291 10.299 4.725 3.276 1.35 6.533 2.692 9.593 4.354 3.195 5.143 8.182 11.962 14.826 11.514.175-.526.331-.974.526-1.5.383.067.787.169 1.199.273 1.743.442 3.611.915 4.509-1.15m130.213-58.45a20.54 20.54 0 0 0 14.504 5.994c5.44 0 10.658-2.156 14.505-5.994a20.44 20.44 0 0 0 6.007-14.469 20.44 20.44 0 0 0-6.007-14.469 20.54 20.54 0 0 0-21.882-4.624l-11.757-17.162-8.194 5.614 11.818 17.25a20.43 20.43 0 0 0-5.002 13.391 20.43 20.43 0 0 0 6.008 14.469m-36.808-55.576a20.55 20.55 0 0 0 21.408-1.964 20.46 20.46 0 0 0 7.34-10.483 20.4 20.4 0 0 0-.331-12.784 20.47 20.47 0 0 0-7.873-10.09 20.557 20.557 0 0 0-18.35-2.282 20.5 20.5 0 0 0-7.964 5.175 20.44 20.44 0 0 0-4.77 8.201 20.429 20.429 0 0 0 3.232 18.164 20.5 20.5 0 0 0 7.308 6.063m0 118.824a20.55 20.55 0 0 0 21.408-1.964 20.46 20.46 0 0 0 7.34-10.483 20.4 20.4 0 0 0-.331-12.783 20.47 20.47 0 0 0-7.873-10.092 20.55 20.55 0 0 0-26.314 2.894 20.45 20.45 0 0 0-4.77 8.201 20.43 20.43 0 0 0 3.232 18.164 20.5 20.5 0 0 0 7.308 6.063m18.857-72.629v-10.174h-31.394a20.3 20.3 0 0 0-4.398-8.342L322.3 88.704l-8.59-5.698-11.812 17.499a20.5 20.5 0 0 0-6.749-1.221 20.5 20.5 0 0 0-14.462 5.959 20.3 20.3 0 0 0-5.99 14.388 20.3 20.3 0 0 0 5.99 14.387 20.5 20.5 0 0 0 14.462 5.96 20.5 20.5 0 0 0 6.749-1.221l11.812 17.498 8.487-5.697-11.709-17.498a20.3 20.3 0 0 0 4.398-8.342z\" clip-rule=\"evenodd\"></path></svg><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/litellm-wordmark-86d6f0432720b27534e05bc579dfc29c.png\" alt=\"LiteLLM\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 4 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->4</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->4<!-- --> earned</span></div><div class=\"skillTracker__series\">Agent orchestration with LangGraph</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">State that survives a restart</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Hand work between agents</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Governed tools an agent can call</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">🏆</span><span class=\"skillTracker__skill\" data-state=\"current\">Engineer the context, not the prompt</span></span></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>La <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/\">Parte 3</a> le dio a un agente cinco\nherramientas por MCP — un calendario, una lista de clientes, facturas y un reembolso — y puso a\nun humano delante del reembolso. Terminó admitiendo lo evidente: <strong>un solo agente tenía las\ncinco.</strong> Nada impedía que el modelo echara mano de <code>issue_refund</code> cuando se le había pedido\nreservar una cita. La pausa humana era lo único que se interponía, y una pausa solo se dispara\nsi el modelo llega a llamar la herramienta.</p>\n<p>Esta parte quita la herramienta en su lugar. El de agenda recibe tres y el de facturación tres,\ncompartiendo una. Pídele al de agenda que reembolse una factura y no puede — no porque se le\nhaya dicho que no, no porque una política lo bloquee, sino porque <code>issue_refund</code> nunca estuvo en\nla lista que se le dio.</p>\n<p>Luego el <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/\">supervisor de la Parte 2</a> vuelve\nencima, y sale a la luz la propiedad interesante: <strong>el enrutado decide quién trabaja, el alcance\ndecide qué es posible.</strong> Un reembolso mal enrutado sigue sin poder reembolsar.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>La misma caja, servidor y base de datos que la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/\">Parte 3</a>: una WEC Instance, Ubuntu 22.04,\nPython 3.10, <code>langchain</code> 1.4.0, <code>langgraph</code> 1.2.11, <code>mcp</code> 2.1.1, <code>fastmcp</code> 4.0.2.\n<code>office_tools.py</code> sin cambios y sirviendo aún cinco herramientas en <code>127.0.0.1:8770</code>. Modelos a\ntravés del gateway LiteLLM hacia WEC Inference. Reinicia la base de datos con\n<code>./.venv/bin/python seed.py</code> antes de seguir, para que la factura 2 vuelva a estar abierta.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-significa-acotar-aquí-en-realidad\">Qué significa \"acotar\" aquí en realidad<a href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/#qu%C3%A9-significa-acotar-aqu%C3%AD-en-realidad\" class=\"hash-link\" aria-label=\"Enlace directo al Qué significa &quot;acotar&quot; aquí en realidad\" title=\"Enlace directo al Qué significa &quot;acotar&quot; aquí en realidad\" translate=\"no\">​</a></h2>\n<p>No hay ningún mecanismo nuevo en este post. <code>create_agent</code> recibe una lista de herramientas;\nnosotros le pasamos una lista más corta. Esa es toda la técnica, y vale la pena ser preciso\nsobre por qué es más fuerte que las alternativas:</p>\n<ul>\n<li class=\"\"><strong>Un prompt de sistema</strong> que le diga al modelo que no emita reembolsos es una petición.\nSobrevive exactamente mientras el modelo colabore.</li>\n<li class=\"\"><strong>Una comprobación de política dentro de la herramienta</strong> es real, pero corre <em>después</em> de que\nel modelo decidiera llamarla, y hay que escribirla en cada herramienta que añadas.</li>\n<li class=\"\"><strong>No pasar la herramienta</strong> elimina la opción del contexto del modelo. No hay nada que\nrechazar, porque no hay nada que llamar.</li>\n</ul>\n<p>La última es la única que no depende de que la inferencia salga bien.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"un-servidor-dos-cajas-de-herramientas\">Un servidor, dos cajas de herramientas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/#un-servidor-dos-cajas-de-herramientas\" class=\"hash-link\" aria-label=\"Enlace directo al Un servidor, dos cajas de herramientas\" title=\"Enlace directo al Un servidor, dos cajas de herramientas\" translate=\"no\">​</a></h2>\n<!-- -->\n<p>El servidor ofrece las cinco a quien pregunte. Lo que cambia es cuáles se le <strong>entregan</strong> a cada\nagente. <code>find_customer</code> va a los dos, porque buscar un cliente es inofensivo.</p>\n<p>La flecha bloqueada es todo el post. El de agenda no puede llamar a <code>issue_refund</code> — no porque\nuna regla se lo prohíba, sino porque la herramienta no está en la lista que se le dio, así que\nnunca llega a su contexto.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">El servidor MCP, el entorno virtual y la base de datos sembrada de la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/\">Parte 3</a></li>\n<li class=\"\">La clave del gateway en <code>~/mcp-tools/.env</code>, como la dejó la Parte 3</li>\n<li class=\"\">Python 3.10+</li>\n</ul>\n<p>La Parte 3 ejecutaba el servidor en primer plano, lo que costaba una terminal y moría con la\nsesión SSH. Arráncalo en segundo plano — con una sola sesión basta para todo lo que sigue:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">nohup</span><span class=\"token plain\"> ./.venv/bin/python office_tools.py </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> server.log </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">3</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> ss </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-tlnp</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8770</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--dos-conjuntos-de-herramientas-desde-un-servidor\">Paso 1 — Dos conjuntos de herramientas desde un servidor<a href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/#paso-1--dos-conjuntos-de-herramientas-desde-un-servidor\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Dos conjuntos de herramientas desde un servidor\" title=\"Enlace directo al Paso 1 — Dos conjuntos de herramientas desde un servidor\" translate=\"no\">​</a></h2>\n<p>El servidor sigue ofreciendo cinco herramientas. Los roles nombran subconjuntos de ellas:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/mcp-tools/scoped.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> sys</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">agents </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> create_agent</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">mcp </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> MCPAdapter</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain_openai </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> ChatOpenAI</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">checkpoint</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">memory </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> InMemorySaver</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">MCP_URL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://127.0.0.1:8770/mcp\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ROLES </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"find_customer\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"list_availability\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"book_slot\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">   </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"find_customer\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"open_invoices\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"issue_refund\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">model </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ChatOpenAI</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    base_url</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"http://127.0.0.1:4000/v1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    api_key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"GATEWAY_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    model</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"qwen-mid\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    temperature</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    role</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> request </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> MCPAdapter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">MCP_URL</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> adapter</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        catalog </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">t</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> t </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> t </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> adapter</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">list_tools</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"server offers : </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation builtin\">sorted</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation\">catalog</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        allowed </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">catalog</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">n</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> n </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> ROLES</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">role</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">role</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> can see : </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation\">t</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">name </span><span class=\"token string-interpolation interpolation keyword\" style=\"color:#00009f\">for</span><span class=\"token string-interpolation interpolation\"> t </span><span class=\"token string-interpolation interpolation keyword\" style=\"color:#00009f\">in</span><span class=\"token string-interpolation interpolation\"> allowed</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\\n\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        agent </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> create_agent</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">model</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> allowed</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> checkpointer</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">InMemorySaver</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        cfg </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"configurable\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"thread_id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">role</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">-1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        out </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> agent</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">ainvoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"role\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"user\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> request</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> cfg</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"__interrupt__\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> out</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            req </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> out</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"__interrupt__\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">value</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"requests\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"PAUSED — the server is asking a human:\\n  </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">req</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'message'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"--- answer ---\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">out</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>El <code>checkpointer</code> viene de la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/\">Parte 3</a>: <code>issue_refund</code> pausa la ejecución con\nun interrupt, y una ejecución interrumpida necesita algún sitio donde esperar. El de agenda nunca\ndispara uno, pero ambos roles ejecutan el mismo código.</p>\n<p>Toda la técnica es una línea:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">allowed </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">catalog</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">n</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> n </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> ROLES</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">role</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><br></div></code></pre></div></div>\n<p><code>find_customer</code> aparece en ambos roles — buscar un cliente es inofensivo y los dos trabajos lo\nnecesitan. <code>book_slot</code> e <code>issue_refund</code> aparecen en exactamente uno cada una.</p>\n<p>El script imprime ambas listas en cada ejecución por una razón. <code>server offers</code> es el catálogo\ncompleto que devolvió el servidor MCP; <code>can see</code> es lo que le pasamos a <code>create_agent</code>. Tenerlas\nuna al lado de la otra deja claro que <strong>el servidor no ocultó nada</strong> — el estrechamiento ocurrió\nen el cliente, en la línea de arriba.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--la-misma-frase-a-los-dos-especialistas\">Paso 2 — La misma frase, a los dos especialistas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/#paso-2--la-misma-frase-a-los-dos-especialistas\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — La misma frase, a los dos especialistas\" title=\"Enlace directo al Paso 2 — La misma frase, a los dos especialistas\" translate=\"no\">​</a></h2>\n<p>Pídele al agente de facturación que haga su trabajo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-a</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">source</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> +a </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python scoped.py billing </span><span class=\"token string\" style=\"color:#e3116c\">\"Refund Maria's open invoice.\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El agente de facturación con find_customer, open_invoices e issue_refund, pausándose con la pregunta de confirmación del servidor sobre la factura 2\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg4-billing-paused-6de8f7f7d8c481db7073918444d88e53.png\" width=\"798\" height=\"204\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Encontró a Maria, resolvió \"factura abierta\" como la factura 2, llamó a <code>issue_refund</code> y se\ndetuvo — porque la herramienta exige un humano. Esa es la guarda de la Parte 3, funcionando.</p>\n<p>Ahora la frase idéntica al especialista equivocado:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python scoped.py scheduler </span><span class=\"token string\" style=\"color:#e3116c\">\"Refund Maria's open invoice.\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El agente de agenda con find_customer, list_availability y book_slot, respondiendo que no tiene herramientas para reembolsos ni facturas\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg4-scheduler-refused-080822f69c551c7655e8e5fa5093178b.png\" width=\"800\" height=\"319\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<blockquote>\n<p>I don't have access to tools that can process refunds or handle invoices. The available tools\nI have are for finding customers, listing appointment availability, and booking appointment\nslots.</p>\n</blockquote>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>La cortesía no es la garantía</div><div class=\"admonitionContent_BuS1\"><p>Esa respuesta es exacta y educada, y sería un error leerla como que el modelo se portó bien. El\nmodelo <strong>no podía</strong> llamar a <code>issue_refund</code>. Nunca estuvo en la lista, así que nunca estuvo en\nel contexto, así que no había ninguna llamada que rechazar.</p><p>La distinción importa porque un modelo que <em>sí</em> tiene una herramienta peligrosa también puede\nproducir una frase educada — incluida una que describa una acción que nunca realizó.\nDocumentamos exactamente eso <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/news/langchain-mcp-first-class/\">en la pieza de noticias</a>: un\nerror de era entregado al modelo, y el modelo narrando una aprobación que nadie iba a pedir\njamás. Un rechazo en el que puedes confiar es uno que el modelo no tenía forma de rodear.</p></div></div>\n<p>Y el de agenda haciendo el trabajo para el que <em>sí</em> tiene herramientas, para que la restricción\nsea dirigida y no paralizante:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python scoped.py scheduler </span><span class=\"token string\" style=\"color:#e3116c\">\"Book Maria into a free slot on 2026-09-05\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El agente de agenda reservando a Maria en el hueco de las 09:00 del 2026-09-05\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg4-scheduler-books-56592bddc8b3d38f6f2f0f8d8aaf64ec.png\" width=\"801\" height=\"226\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--comprobar-que-nada-se-movió\">Paso 3 — Comprobar que nada se movió<a href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/#paso-3--comprobar-que-nada-se-movi%C3%B3\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — Comprobar que nada se movió\" title=\"Enlace directo al Paso 3 — Comprobar que nada se movió\" translate=\"no\">​</a></h2>\n<p>Un rechazo es una frase. La base de datos es la evidencia.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">import sqlite3</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for r in sqlite3.connect('office.db').execute('select id, customer_id, amount, status from invoices'): print(r)\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La tabla de facturas mostrando la factura 2 todavía abierta tras el rechazo y la ejecución pausada\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg4-nothing-happened-3c326c0194b72bcc27c1a87d9a2d05f7.png\" width=\"790\" height=\"145\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>La factura 2 sigue <code>open</code>, por dos razones distintas: el de agenda no tenía herramienta para\ncambiarla, y el de facturación fue detenido por la puerta humana antes de poder hacerlo. Ninguna\ndependió de que el modelo eligiera bien.</p>\n<p>Vale la pena ver también al mismo agente informar de una verdad aburrida. Antes de reiniciar la\nbase de datos, la factura 2 ya se había reembolsado en la Parte 3 — y al pedirle reembolsarla de\nnuevo, facturación miró, no encontró nada abierto, y lo dijo:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El agente de facturación informando de que Maria no tiene facturas abiertas, que están pagadas o ya reembolsadas\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg4-billing-nothing-open-b4f526ef9e6865039b0aefe2909f7918.png\" width=\"799\" height=\"207\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Sin factura inventada, sin confirmación entusiasta. Ese es el comportamiento que hace que sus\notras respuestas valgan algo, y no es algo que dar por hecho — es algo que comprobar, y por eso\ncada afirmación de este post tiene una consulta detrás.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes dar a dos agentes porciones distintas del mismo servidor MCP, de modo que una capacidad\nque uno de ellos nunca debe tener esté ausente de su contexto en lugar de prohibida por\ninstrucción — y verificar en los datos que la ausencia se sostuvo.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--el-supervisor-de-vuelta-encima\">Paso 4 — El supervisor, de vuelta encima<a href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/#paso-4--el-supervisor-de-vuelta-encima\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — El supervisor, de vuelta encima\" title=\"Enlace directo al Paso 4 — El supervisor, de vuelta encima\" translate=\"no\">​</a></h2>\n<p>La Parte 2 repartía trabajo entre un agente de agenda y uno de facturación que no tenían\nherramientas. Ahora las tienen.</p>\n<p><code>supervisor.py</code> es <code>scoped.py</code> con el grafo de la Parte 2 alrededor: los mismos <code>ROLES</code>,\n<code>MCPAdapter</code> y <code>ChatOpenAI</code>, más un <code>State</code> TypedDict, un <code>StateGraph</code>, y un agente construido\npor rol dentro del bloque <code>async with</code> — <code>agents = {role: create_agent(model, [catalog[n] for n in names]) for role, names in ROLES.items()}</code>. Aquí solo se muestran las partes nuevas de este\npost.</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/mcp-tools/supervisor.py (las partes que importan)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">BILLING_WORDS </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"invoice\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"refund\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"bill\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"charge\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"payment\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">supervisor</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">Literal</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    nxt </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">any</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">w </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"request\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">lower</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> w </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> BILLING_WORDS</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"supervisor routes to </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">nxt</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">goto</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">nxt</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> update</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"supervisor-&gt;</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">nxt</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">role</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    out </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> agents</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">role</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">ainvoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"role\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"user\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"request\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"__interrupt__\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> out</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        q </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> out</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"__interrupt__\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">value</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"requests\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"message\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"answer\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"PAUSED — </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">q</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">role</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"answer\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> out</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">role</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">scheduler_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">billing_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>La anotación <code>Command[Literal[\"scheduler\", \"billing\"]]</code> hace el mismo trabajo que en la Parte 2 —\nes la única declaración de a dónde le está permitido al supervisor mandar trabajo, y quitarla\nhace que todos los diagramas del grafo mientan.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-a</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">source</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> +a </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python supervisor.py </span><span class=\"token string\" style=\"color:#e3116c\">\"Refund Maria's open invoice.\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El supervisor enrutando una petición de reembolso al agente de facturación, que se pausa en la puerta humana\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg4-supervisor-refund-eff231a6a04d02fc5430fa25025750e6.png\" width=\"801\" height=\"226\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python supervisor.py </span><span class=\"token string\" style=\"color:#e3116c\">\"Book Maria into a free slot on 2026-09-04\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El supervisor enrutando una petición de reserva al agente de agenda, que reserva el hueco\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg4-supervisor-book-cb9829d31562e18281b2165e7fb3bf6e.png\" width=\"800\" height=\"244\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>El enrutado no es la garantía</div><div class=\"admonitionContent_BuS1\"><p>Ese supervisor es una coincidencia de palabras clave. Es trivialmente engañable — \"salda el\nimporte pendiente de Maria\" no contiene ninguna de <code>BILLING_WORDS</code> y aterriza en el de agenda.</p><p>Que es justo lo que hay que llevarse. <strong>Un reembolso mal enrutado sigue sin poder reembolsar.</strong>\nEl enrutador decide quién recibe el trabajo; los conjuntos de herramientas deciden qué puede\nhacer ese agente con él. Si el enrutado fuera el único control, cada fallo del supervisor sería\nun fallo de seguridad. Con el alcance por debajo, un error de enrutado es solo una mala\nrespuesta.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes poner un enrutador delante de especialistas acotados y razonar con claridad sobre qué\nfallos son peligrosos — porque el radio de impacto de un error de enrutado está limitado por lo\nque se le dio al agente receptor, no por lo que se le pidió hacer.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"resolución-de-problemas--los-errores-que-esta-ejecución-produjo-de-verdad\">Resolución de problemas — los errores que esta ejecución produjo de verdad<a href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/#resoluci%C3%B3n-de-problemas--los-errores-que-esta-ejecuci%C3%B3n-produjo-de-verdad\" class=\"hash-link\" aria-label=\"Enlace directo al Resolución de problemas — los errores que esta ejecución produjo de verdad\" title=\"Enlace directo al Resolución de problemas — los errores que esta ejecución produjo de verdad\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"invalidupdateerror-expected-dict-got-coroutine-object\"><code>InvalidUpdateError: Expected dict, got &lt;coroutine object&gt;</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/#invalidupdateerror-expected-dict-got-coroutine-object\" class=\"hash-link\" aria-label=\"Enlace directo al invalidupdateerror-expected-dict-got-coroutine-object\" title=\"Enlace directo al invalidupdateerror-expected-dict-got-coroutine-object\" translate=\"no\">​</a></h3>\n<p>El nodo es una función <code>async</code> envuelta en un <code>lambda</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">lambda</span><span class=\"token plain\"> s</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> s</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\">   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># devuelve una corrutina</span><br></div></code></pre></div></div>\n<p>LangGraph lo llama, recibe una corrutina en lugar de una actualización de estado, y lanza el\nerror. La pista real es la última línea de la salida — <code>RuntimeWarning: coroutine 'run' was never awaited</code> — que es la parte que nadie lee. Define nodos <code>async def</code> de verdad.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"runtimeerror-client-failed-to-connect-all-connection-attempts-failed\"><code>RuntimeError: Client failed to connect: All connection attempts failed</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/#runtimeerror-client-failed-to-connect-all-connection-attempts-failed\" class=\"hash-link\" aria-label=\"Enlace directo al runtimeerror-client-failed-to-connect-all-connection-attempts-failed\" title=\"Enlace directo al runtimeerror-client-failed-to-connect-all-connection-attempts-failed\" translate=\"no\">​</a></h3>\n<p>No hay nada escuchando en 8770. Si arrancaste el servidor MCP en un shell en primer plano, murió\ncon tu sesión SSH. El error nombra al <em>cliente</em>, lo que te manda a buscar en el sitio\nequivocado.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ss </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-tlnp</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8770</span><br></div></code></pre></div></div>\n<p>Arráncalo con <code>nohup ... &amp;</code> para que sobreviva, y comprueba el puerto antes de culpar al código.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"una-ejecución-en-segundo-plano-no-imprime-nada-durante-minutos\">Una ejecución en segundo plano no imprime nada durante minutos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/#una-ejecuci%C3%B3n-en-segundo-plano-no-imprime-nada-durante-minutos\" class=\"hash-link\" aria-label=\"Enlace directo al Una ejecución en segundo plano no imprime nada durante minutos\" title=\"Enlace directo al Una ejecución en segundo plano no imprime nada durante minutos\" translate=\"no\">​</a></h3>\n<p>Python almacena la salida en búfer cuando se redirige a un archivo, así que una ejecución larga\nparece idéntica a un cuelgue. Añade <code>-u</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">nohup</span><span class=\"token plain\"> ./.venv/bin/python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-u</span><span class=\"token plain\"> supervisor.py </span><span class=\"token string\" style=\"color:#e3116c\">\"...\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> run.log </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-f</span><span class=\"token plain\"> run.log</span><br></div></code></pre></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"keyerror-gateway_key\"><code>KeyError: 'GATEWAY_KEY'</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/#keyerror-gateway_key\" class=\"hash-link\" aria-label=\"Enlace directo al keyerror-gateway_key\" title=\"Enlace directo al keyerror-gateway_key\" translate=\"no\">​</a></h3>\n<p><code>source .env</code> define variables de shell, no variables de entorno, y <code>export</code> no cruza entre\nsesiones SSH. Cada comando que arranca el agente necesita su propio\n<code>set -a &amp;&amp; source .env &amp;&amp; set +a</code> — incluidos los que van en segundo plano.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-te-compró-esto-y-qué-no\">Qué te compró esto y qué no<a href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/#qu%C3%A9-te-compr%C3%B3-esto-y-qu%C3%A9-no\" class=\"hash-link\" aria-label=\"Enlace directo al Qué te compró esto y qué no\" title=\"Enlace directo al Qué te compró esto y qué no\" translate=\"no\">​</a></h2>\n<p>Hecho: dos agentes tomando subconjuntos distintos de un mismo servidor MCP, una capacidad que\nestá ausente en lugar de prohibida, y un enrutador cuyos errores cuestan corrección en vez de\ndinero.</p>\n<p>No hecho:</p>\n<ul>\n<li class=\"\"><strong>El servidor MCP sigue confiando en todo el mundo.</strong> El acotado ocurre en el <em>cliente</em>.\nCualquier cosa que alcance <code>127.0.0.1:8770</code> puede llamar a <code>issue_refund</code> directamente, sea un\nagente o no. Esto controla lo que tus agentes pueden hacer, no lo que tu servidor aceptará.</li>\n<li class=\"\"><strong>Sin identidad en la llamada a herramienta.</strong> El servidor no puede distinguir al de agenda del\nde facturación, porque nada en el protocolo dice cuál está llamando.</li>\n<li class=\"\"><strong>Los subconjuntos están escritos a mano.</strong> Un despliegue real los lee del mismo sitio del que\nlee la pertenencia a equipos — que es territorio de la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/\">serie de endurecimiento</a>, una capa más\nabajo.</li>\n<li class=\"\"><strong>El supervisor es una coincidencia de palabras clave</strong>, mantenido deliberadamente tonto para\nque el argumento del acotado se sostenga solo.</li>\n</ul>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Engineer the context, not the prompt</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-sigue\">Qué sigue<a href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al Qué sigue\" title=\"Enlace directo al Qué sigue\" translate=\"no\">​</a></h2>\n<p>Empujar el límite del cliente al servidor: <strong>autenticar la llamada a herramienta</strong>, para que el\nservidor sepa qué agente pregunta y le niegue <code>issue_refund</code> a cualquiera que no sea\nfacturación. Esa es la misma pregunta que la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/\">Parte 4 de la serie de endurecimiento</a> hizo\nsobre la propia API del gateway — un plano de control y un plano de datos, a una capa de\ndistancia — y FastMCP ya soporta tokens bearer y OAuth 2.1 para ello.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lecturas-adicionales\">Lecturas adicionales<a href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/#lecturas-adicionales\" class=\"hash-link\" aria-label=\"Enlace directo al Lecturas adicionales\" title=\"Enlace directo al Lecturas adicionales\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.langchain.com/oss/python/langchain/mcp\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">MCP en LangChain</a> — conexiones, auth, elicitación</li>\n<li class=\"\"><a href=\"https://gofastmcp.com/clients/auth/oauth\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">FastMCP — autenticación del cliente</a></li>\n<li class=\"\"><a href=\"https://docs.langchain.com/oss/python/langgraph/graph-api\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">LangGraph — <code>Command</code> y aristas condicionales</a></li>\n<li class=\"\"><a href=\"https://modelcontextprotocol.io/specification/2026-07-28\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Especificación MCP <code>2026-07-28</code></a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/",
            "title": "Repartir una caja de herramientas MCP entre dos agentes para que el de agenda no pueda emitir reembolsos",
            "summary": "La Parte 3 le dio a un solo agente las cinco herramientas y dejó que una pausa humana vigilara la peligrosa. Esa guarda depende de que el modelo pregunte. Esto reparte el mismo servidor MCP entre un agente de agenda y uno de facturación, de modo que el de agenda no puede reembolsar una factura por la razón más simple que existe: la herramienta nunca estuvo en su lista. Luego el supervisor vuelve encima, y resulta que el enrutado no es la garantía.",
            "date_modified": "2026-09-04T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "agentes",
                "langgraph",
                "langchain",
                "mcp",
                "herramientas",
                "seguridad",
                "autoalojamiento",
                "wec",
                "wec-inference"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__authentik\" src=\"data:image/png;base64,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\" alt=\"authentik\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/litellm-wordmark-86d6f0432720b27534e05bc579dfc29c.png\" alt=\"LiteLLM\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Lock down Docker networks</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run containers as non-root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">4</span><span class=\"skillTracker__skill\" data-state=\"current\">Membership, not just an account</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">An identity for the agent, not a key</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">A tool server that checks who is asking</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Where the agent can go, not just what it can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/\"><span class=\"skillTracker__dot\" data-state=\"locked\">8</span><span class=\"skillTracker__skill\" data-state=\"locked\">Move the daemon off root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/\"><span class=\"skillTracker__dot\" data-state=\"locked\">9</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run the model's own code without trusting it</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">A scope per tool, and a refusal clients can act on</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>La <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/\">Parte 3</a> puso authentik delante de Langfuse y\nterminó con una confesión: el paso de vinculaciones quedó vacío, así que cualquier cuenta de\nauthentik podía llegar a Langfuse y nada te avisaría.</p>\n<p>Esta parte cierra ese hueco en la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/\">gateway LiteLLM</a> — y el cierre salió mal de una\nforma que vale más que el procedimiento. Configuramos las vinculaciones en el asistente de\nauthentik, las vimos aparecer en la tabla del propio asistente, enviamos, y acabamos con\n<strong>cero vinculaciones guardadas</strong>. La aplicación estaba abierta a todo el mundo, la interfaz\nno dijo nada, y la única razón por la que lo descubrimos es que probamos con una cuenta que\ndebería haber sido rechazada y no lo fue.</p>\n<p>Después, ya con el control de acceso funcionando de verdad, llamamos a la API del gateway\ndesde un shell sin cuenta, sin sesión y sin grupo. Respondió con normalidad. Eso no es un\nfallo — es la diferencia entre un <strong>plano de control</strong> y un <strong>plano de datos</strong>, y suponer que\nel SSO cubre ambos es cómo la gente concluye que ha asegurado algo que no ha asegurado.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>Una WEC Instance: 8 vCPU AMD EPYC 7601, 15 GB RAM, Docker 29.1.3, kernel 5.15.\nauthentik <code>2026.8.1</code>, la misma instancia de la Parte 3 en el puerto host <code>9100</code>. LiteLLM\n<code>main-stable</code> de la <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/\">Parte 1 de la serie del gateway</a>,\nenlazado a <code>127.0.0.1:4000</code>, con modelos servidos por WEC Inference.</p><p>HTTP plano sobre una LAN privada, como en la Parte 3.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-hay-de-nuevo-aquí\">Qué hay de nuevo aquí<a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#qu%C3%A9-hay-de-nuevo-aqu%C3%AD\" class=\"hash-link\" aria-label=\"Enlace directo al Qué hay de nuevo aquí\" title=\"Enlace directo al Qué hay de nuevo aquí\" translate=\"no\">​</a></h2>\n<p>La Parte 3 enseñó el asistente — aplicación, proveedor, URI de redirección, scopes. Eso no se\nrepite; <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/\">está todo en la Parte 3</a> y los pasos son\nidénticos para cualquier app. Tres cosas son nuevas:</p>\n<p><strong>Un grupo</strong>, como la unidad a la que concedes acceso, de modo que añadir a alguien a un\nequipo y darle acceso sean la misma acción.</p>\n<p><strong>Una vinculación</strong>, que ata ese grupo a una aplicación, de modo que llegar a ella exija\npertenencia en lugar de meramente existir.</p>\n<p><strong>Un límite</strong>, demostrado en vez de afirmado: qué protege el SSO, y qué no toca en absoluto.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">Un authentik funcionando, idealmente el de la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/\">Parte 3</a></li>\n<li class=\"\">Un gateway LiteLLM en ejecución, de la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/\">Parte 1 de la serie del gateway</a></li>\n<li class=\"\">Su <code>LITELLM_MASTER_KEY</code>, que necesitarás para crear una clave virtual</li>\n<li class=\"\">Acceso SSH a la caja, porque el gateway está enlazado a <code>127.0.0.1</code></li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"dos-puertas-solo-una-vigilada\">Dos puertas, solo una vigilada<a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#dos-puertas-solo-una-vigilada\" class=\"hash-link\" aria-label=\"Enlace directo al Dos puertas, solo una vigilada\" title=\"Enlace directo al Dos puertas, solo una vigilada\" translate=\"no\">​</a></h2>\n<!-- -->\n<p>Dos cajas, sin flecha entre ellas — ese es el punto. Todo lo que configura este post vive en\nla de la izquierda. La de la derecha nunca consulta a authentik.</p>\n<p>También hay una tercera vía de entrada que el diagrama deja fuera a propósito, porque no\npertenece a ninguno de los dos planos: la <strong>clave maestra</strong> de LiteLLM entra directamente a\nla interfaz de administración, saltándose por completo la comprobación de grupo. Nos topamos\ncon ella en el Paso 5.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--un-grupo-y-dos-cuentas\">Paso 1 — Un grupo y dos cuentas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#paso-1--un-grupo-y-dos-cuentas\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Un grupo y dos cuentas\" title=\"Enlace directo al Paso 1 — Un grupo y dos cuentas\" translate=\"no\">​</a></h2>\n<p><strong>Directory → Groups → New Group</strong>, llámalo <code>gateway-admins</code>, y deja <strong>Superuser privileges</strong>\ndesactivado. Esa bandera convierte a los miembros en administradores <em>del propio authentik</em> y\nno tiene nada que ver con qué aplicaciones pueden alcanzar.</p>\n<p>authentik ya trae tres grupos — <code>authentik Admins</code>, <code>authentik Agent-Users</code>,\n<code>authentik Read-only</code> — y <code>authentik Admins</code> ya contiene tu <code>akadmin</code>. Vincular ese\nfuncionaría hoy y significaría en silencio \"cualquiera a quien alguna vez haga administrador\nde authentik\", que no es la misma intención.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La lista de grupos de authentik mostrando solo los tres grupos integrados, con authentik Admins ya con un miembro\" src=\"https://development-wec.wiline.com/docs/es/assets/images/ak-groups-list-a17357dbbd4468da8f7232d5487ef77a.png\" width=\"2788\" height=\"1334\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Añade <code>akadmin</code> a <code>gateway-admins</code>. Luego <strong>Directory → Users → New User</strong>, tipo <strong>Internal</strong>,\nusuario <code>contractor</code>, y <strong>sin ningún grupo</strong>.</p>\n<p>Internal y no External para que la única diferencia entre las dos cuentas sea la pertenencia\n— cualquier otra diferencia enturbia el resultado. Tampoco Service Account: esas son de\nmáquina a máquina y no hacen inicios de sesión interactivos.</p>\n<p>authentik crea usuarios sin contraseña, y el diálogo de edición no tiene campo de contraseña\n— es una acción aparte en la página de detalle del usuario. Desde el shell:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.core.models import User</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">u = User.objects.get(username='contractor')</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">u.set_password('Contractor123!')</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">u.save()</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('password set for', u.username)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-2</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Silenciar <code>ak shell</code></div><div class=\"admonitionContent_BuS1\"><p>Cada llamada a <code>ak shell</code> imprime unas sesenta líneas de JSON de arranque antes de tu salida.\n<code>2&gt;/dev/null | tail -N</code> descarta el ruido y conserva las últimas N líneas. Se usa en todo lo\nque sigue.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--registrar-el-gateway-y-vigilar-el-slug\">Paso 2 — Registrar el gateway, y vigilar el slug<a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#paso-2--registrar-el-gateway-y-vigilar-el-slug\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — Registrar el gateway, y vigilar el slug\" title=\"Enlace directo al Paso 2 — Registrar el gateway, y vigilar el slug\" translate=\"no\">​</a></h2>\n<p>Ejecuta el asistente de la Parte 3: aplicación <strong>LiteLLM</strong>, proveedor <strong>OAuth2/OpenID</strong>, flujo\nde autorización <code>default-provider-authorization-explicit-consent</code>, tipo de cliente\n<strong>Confidential</strong>.</p>\n<p>El gateway está publicado en <code>127.0.0.1:4000</code>, así que tu navegador no puede alcanzarlo — y\nun inicio de sesión OIDC ocurre en un navegador. Abre un túnel desde tu equipo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">ssh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-L</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">4000</span><span class=\"token plain\">:127.0.0.1:4000 ubuntu@10.80.4.212</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span><code>ssh -L</code> avisa, y luego sigue sin el túnel</div><div class=\"admonitionContent_BuS1\"><p>Si algo ya ocupa el puerto local 4000, ssh imprime <code>bind: Address already in use</code> y aun así\nte da un shell funcionando — con el reenvío muerto. El único síntoma es\n<code>ERR_CONNECTION_REFUSED</code> en el navegador, que parece que el gateway estuviera caído.\nComprueba el puerto antes de culpar al gateway: <code>lsof -nP -iTCP:4000 -sTCP:LISTEN</code>.</p></div></div>\n<p>Todo lo que el navegador escribe es ahora <code>localhost:4000</code>, así que esa es la URI de\nredirección, modo <strong>Strict</strong>:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">http://localhost:4000/sso/callback</span><br></div></code></pre></div></div>\n<p>Ahora la primera sorpresa. Pregúntale a authentik cómo llamó realmente a las cosas:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.core.models import Application</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for a in Application.objects.all():</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print(repr(a.name), '-&gt;', repr(a.slug))</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-5</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">'Langfuse' -&gt; 'langfuse'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">'LiteLLM' -&gt; 'lite-llm'</span><br></div></code></pre></div></div>\n<p><strong><code>lite-llm</code>, no <code>litellm</code>.</strong> El generador de slugs leyó las mayúsculas de \"LiteLLM\" y las\nseparó. La Parte 3 dijo que el slug carga peso porque acaba en la URL del emisor, y aquí está:\n<code>http://10.80.4.212:9100/application/o/lite-llm/</code>. Fija el slug explícitamente en lugar de\ndejar que un nombre en CamelCase lo genere, o carga ese guion en cada URL para siempre.</p>\n<p>Lee las credenciales con el slug real:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.core.models import Application</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">p = Application.objects.get(slug='lite-llm').get_provider()</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('CLIENT_ID    :', p.client_id)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('CLIENT_SECRET:', p.client_secret)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-3</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--las-vinculaciones-que-el-asistente-tiró\">Paso 3 — Las vinculaciones que el asistente tiró<a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#paso-3--las-vinculaciones-que-el-asistente-tir%C3%B3\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — Las vinculaciones que el asistente tiró\" title=\"Enlace directo al Paso 3 — Las vinculaciones que el asistente tiró\" translate=\"no\">​</a></h2>\n<p>El paso 4 del asistente es <strong>Configure Bindings</strong>, y la Parte 3 pasó de largo mostrando\n<code>No bound policies.</code> Esta vez no pasamos de largo. Vinculamos el grupo ahí, el asistente lo\nlistó en su tabla, y enviamos.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El paso Configure Bindings del asistente, vacío, indicando No bound policies\" src=\"https://development-wec.wiline.com/docs/es/assets/images/ak-bindings-wizard-empty-996a2adb20faf53dda84769592740e1c.png\" width=\"1800\" height=\"1033\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El mismo paso del asistente tras vincular, listando Group gateway-admins como habilitado\" src=\"https://development-wec.wiline.com/docs/es/assets/images/ak-bindings-wizard-group-2c88ba715a495db175a3e85cf83df1f9.png\" width=\"1800\" height=\"1025\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Luego le preguntamos a la base de datos qué existía:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.core.models import Application</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">app = Application.objects.get(slug='lite-llm')</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">bs = list(app.bindings.all())</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('binding count:', len(bs))</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for b in bs:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print('group', b.group, '| enabled', b.enabled, '| negate', b.negate)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-4</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">binding count: 0</span><br></div></code></pre></div></div>\n<p>Cero. La propia pestaña de vinculaciones de la aplicación coincidía — <strong>No Policies bound.</strong>\nDos fuentes independientes, una conclusión: el asistente mostró un control de acceso que\nnunca se escribió.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La pestaña de vinculaciones de la aplicación LiteLLM indicando No Policies bound\" src=\"https://development-wec.wiline.com/docs/es/assets/images/ak-bindings-none-71e30bfc7aa65339a119ee1ccb38d3ab.png\" width=\"1896\" height=\"901\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-danger admonition_xJq3 alert alert--danger\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z\"></path></svg></span>Esto es peor que saltarse el paso</div><div class=\"admonitionContent_BuS1\"><p>La lección de la Parte 3 era \"no te saltes las vinculaciones\". El peligro real es que puedes\n<em>completarlas</em>, ver tus reglas en la tabla del asistente, enviar, y acabar con una aplicación\nque admite a todo el mundo — sin error, sin aviso, y con una interfaz que se veía correcta\ntodo el rato.</p><p>Una aplicación con cero vinculaciones no está rota de ninguna forma que authentik reporte.\nEstá haciendo exactamente lo que debe: nada vinculado significa que nadie queda excluido.\n<strong>Verifica las vinculaciones desde la base de datos o desde la propia pestaña de\nvinculaciones de la aplicación — nunca desde el asistente.</strong></p></div></div>\n<p>Vincúlalo desde la aplicación en su lugar: <strong>Applications → LiteLLM → Policy / Group / User\nBindings → Create or bind…</strong>, luego <strong>Bind a group</strong> bajo <em>Bind Existing…</em> → <code>gateway-admins</code>\n→ <strong>Create</strong>.</p>\n<p>Elige <strong>Bind a group</strong> en concreto. La sección <em>Choose Policy Type</em> de abajo crea políticas\nnuevas (Event Matcher, Expression, GeoIP); una vinculación de grupo ya es una regla y no\nnecesita ninguna de ellas.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El diálogo Create Binding con gateway-admins seleccionado, Enabled activado, Negate Result desactivado y Failure Result en Don&amp;#39;t Pass\" src=\"https://development-wec.wiline.com/docs/es/assets/images/ak-bind-group-dialog-c20ad52e0f590a4df1c9a5e3aa411963.png\" width=\"1897\" height=\"945\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Luego verifica, porque la tabla es lo que mintió:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">binding count: 1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">group Group gateway-admins | enabled True | negate False</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes restringir una aplicación a un grupo en lugar de dejarla abierta a toda cuenta — y,\nmás importante, verificar que la vinculación realmente persistió (<code>app.bindings.all()</code>) en\nvez de fiarte de la tabla del propio asistente.</p></div></div>\n<p>Esa página indica el modo con claridad: <em>\"The currently selected policy engine mode is ANY:\nAny policy must match to grant access.\"</em> Con una sola vinculación, ANY y ALL se comportan\nigual. Con dos no — <strong>ANY significa que basta con que una regla pase</strong>, así que una\nvinculación suelta de <code>User Contractor</code> junto al grupo concedería exactamente el acceso que\neste post intenta denegar.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--apuntar-el-gateway-hacia-authentik\">Paso 4 — Apuntar el gateway hacia authentik<a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#paso-4--apuntar-el-gateway-hacia-authentik\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — Apuntar el gateway hacia authentik\" title=\"Enlace directo al Paso 4 — Apuntar el gateway hacia authentik\" translate=\"no\">​</a></h2>\n<p>LiteLLM lee ajustes OIDC genéricos desde <code>GENERIC_*</code>. Los endpoints salen del documento de\ndescubrimiento:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;&gt;</span><span class=\"token plain\"> ~/llm-gateway/.env </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">GENERIC_CLIENT_ID=&lt;client id del Paso 2&gt;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">GENERIC_CLIENT_SECRET=&lt;client secret del Paso 2&gt;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">GENERIC_AUTHORIZATION_ENDPOINT=http://10.80.4.212:9100/application/o/authorize/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">GENERIC_TOKEN_ENDPOINT=http://10.80.4.212:9100/application/o/token/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">GENERIC_USERINFO_ENDPOINT=http://10.80.4.212:9100/application/o/userinfo/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">PROXY_BASE_URL=http://localhost:4000</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><br></div></code></pre></div></div>\n<p><code>PROXY_BASE_URL</code> es la que muerde. LiteLLM construye su URI de redirección a partir de ella,\nasí que tiene que ser igual a lo que el navegador escribe y a lo que authentik tiene\nregistrado. Las nuestras no coincidieron en el primer intento y produjeron un <strong>Redirect URI\nError</strong> — el <code>redirect_uri</code> de la barra de direcciones es lo que LiteLLM envió, el valor\nStrict del proveedor es lo que authentik esperaba, y basta un puerto, un nombre de host o una\nbarra final entre ambos para fallar.</p>\n<p>El archivo compose del gateway usa <code>env_file: .env</code>, así que a diferencia de Langfuse en la\nParte 3 no hay archivo de override que escribir:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/llm-gateway </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> litellm </span><span class=\"token function\" style=\"color:#d73a49\">env</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> GENERIC_</span><br></div></code></pre></div></div>\n<p>Cinco. Antes de la edición era cero — la misma clase de comprobación que la Parte 3 usó para\ncazar variables que nunca llegaban al proceso.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5--los-dos-lados-de-la-vinculación\">Paso 5 — Los dos lados de la vinculación<a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#paso-5--los-dos-lados-de-la-vinculaci%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 5 — Los dos lados de la vinculación\" title=\"Enlace directo al Paso 5 — Los dos lados de la vinculación\" translate=\"no\">​</a></h2>\n<p>Abre <code>http://localhost:4000/ui</code> a través del túnel. La página de inicio de sesión trae un\nhallazgo que este post no fue a buscar.</p>\n<p><strong>Login with SSO</strong> aparece, como se pretendía. Encima hay un formulario de usuario y\ncontraseña, y el propio panel de LiteLLM lo explica: <em>\"By default, Username is <code>admin</code> and\nPassword is your set LiteLLM Proxy <code>MASTER_KEY</code>.\"</em></p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La página de inicio de sesión de LiteLLM mostrando un formulario de usuario y contraseña encima del botón Login with SSO, con un panel que nombra la clave maestra como contraseña por defecto\" src=\"https://development-wec.wiline.com/docs/es/assets/images/litellm-login-sso-a0616071a645fb27014bfa0b35203eaa.png\" width=\"1903\" height=\"997\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>La clave maestra es un bypass completo. Cualquiera que la tenga llega a la interfaz de\nadministración sin tocar authentik, sin grupo, sin vinculación. Todo lo configurado en los\nPasos 1–3 está junto a una puerta que lo ignora — y la llave de esa puerta es además la\ncredencial raíz del gateway, capaz de acuñar claves de API y alcanzar cualquier modelo.</p>\n<p><code>AUTO_REDIRECT_UI_LOGIN_TO_SSO=true</code> manda <code>/ui</code> directo a authentik en lugar de mostrar el\nformulario. Deja de <em>anunciar</em> la vía de contraseña; no la elimina. Trata la clave maestra\ncomo una credencial de emergencia y no confundas el redirect con una solución.</p>\n<p>Inicia sesión como <code>akadmin</code>, que está en <code>gateway-admins</code>. authentik muestra la pantalla de\nconsentimiento del flujo explícito de la Parte 3, y aterrizas en el panel en\n<code>/ui/?login=success</code>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La pantalla de inicio de sesión de authentik indicando Log in to continue to LiteLLM\" src=\"https://development-wec.wiline.com/docs/es/assets/images/ak-login-litellm-9e4c240eccffd4f7e739199f9b9c594a.png\" width=\"1800\" height=\"797\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La pantalla de consentimiento para akadmin, listando Email address y General Profile Information\" src=\"https://development-wec.wiline.com/docs/es/assets/images/ak-consent-akadmin-ea2ed59007a2b3e896f2d0437553763f.png\" width=\"1800\" height=\"774\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Ahora <code>contractor</code>.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Todas las ventanas de incógnito comparten una sesión</div><div class=\"admonitionContent_BuS1\"><p>Abrir \"una ventana privada nueva\" mientras ya hay otra abierta reutiliza las mismas cookies,\nasí que authentik te vuelve a identificar como el usuario anterior. El síntoma es un inicio\nde sesión instantáneo que parece un fallo de tu control de acceso. Cierra <strong>todas</strong> las\nventanas de incógnito, o usa otro navegador.</p></div></div>\n<p><strong>Permission denied. Request has been denied.</strong></p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"authentik rechazando a contractor con Permission denied y Request has been denied\" src=\"https://development-wec.wiline.com/docs/es/assets/images/ak-contractor-denied-14fb1a97964d42b993f3e49504ecb9dd.png\" width=\"1761\" height=\"694\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Fíjate en dónde ocurrió: authentik rechazó <em>antes</em> de la pantalla de consentimiento. La\ncomprobación de política corre por delante del consentimiento, así que un usuario bloqueado\nnunca ve la página de permisos — que es también cómo cazamos las vinculaciones ausentes antes.\nCuando <code>contractor</code> llegó a una pantalla de consentimiento, esa fue la pista.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-parte-que-no-queda-cerrada\">La parte que no queda cerrada<a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#la-parte-que-no-queda-cerrada\" class=\"hash-link\" aria-label=\"Enlace directo al La parte que no queda cerrada\" title=\"Enlace directo al La parte que no queda cerrada\" translate=\"no\">​</a></h2>\n<p>Desde un shell en la caja — sin cuenta de authentik, sin sesión, sin grupo, nada más que una\nclave:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer sk-...\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-mid\",\"messages\":[{\"role\":\"user\",\"content\":\"Say OK.\"}]}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token string\" style=\"color:#e3116c\">'{model, answer: .choices[0].message.content}'</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"model\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"qwen-mid\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"answer\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\\n\\nOK\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La llamada curl respondiendo con normalidad desde un shell sin sesión de authentik\" src=\"https://development-wec.wiline.com/docs/es/assets/images/litellm-api-still-open-f3c81e08b6e2981f5cd844d8f236a4ec.png\" width=\"532\" height=\"186\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Nada está roto. <strong>El SSO nunca se aplicó a ese endpoint.</strong> Los Pasos 1–5 protegieron <code>/ui</code> —\nel plano de control, donde las personas crean claves y leen el gasto. El plano de datos en\n<code>/v1/*</code> autentica a los llamantes máquina con claves de API, y tiene que hacerlo: un agente\ncorriendo a las 3 de la madrugada no puede completar un login de navegador con pantalla de\nconsentimiento.</p>\n<p>Esa clave se creó a su vez desde un shell, con la clave maestra, sin que existiera ninguna\nsesión SSO en ningún sitio:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/llm-gateway </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">source</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/key/generate </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$LITELLM_MASTER_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"key_alias\":\"sso-demo\",\"models\":[]}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.key'</span><br></div></code></pre></div></div>\n<p>Las consecuencias, dichas sin rodeos, porque \"le pusimos SSO al gateway\" se dice en\nreuniones como si zanjara la cuestión:</p>\n<ul>\n<li class=\"\"><strong>Sacar a alguien de <code>gateway-admins</code> no revoca sus claves.</strong> Pierde la interfaz y conserva\ntodas las claves que creó. Dar de baja son dos acciones; solo una es la que la gente\nrecuerda.</li>\n<li class=\"\"><strong>Una clave filtrada no se ve afectada por la identidad en absoluto.</strong> No hay sesión que\nexpirar ni grupo del que sacarla.</li>\n<li class=\"\"><strong>Las claves sobreviven a las personas</strong>, salvo que les dieras dueño y caducidad al crearlas.</li>\n</ul>\n<p>El propio modelo de claves del gateway — claves acotadas, presupuestos, caducidad, modelos\npor clave — es lo que cubre ese terreno, y es un mecanismo distinto del configurado aquí.\nComplementario, no sustituible.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"una-capa-más-entrar-no-es-poder-hacer-nada\">Una capa más: entrar no es poder hacer nada<a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#una-capa-m%C3%A1s-entrar-no-es-poder-hacer-nada\" class=\"hash-link\" aria-label=\"Enlace directo al Una capa más: entrar no es poder hacer nada\" title=\"Enlace directo al Una capa más: entrar no es poder hacer nada\" translate=\"no\">​</a></h2>\n<p>Con la sesión iniciada como <code>akadmin</code> vía SSO, la página de Virtual Keys no tenía botón\n<strong>Create Key</strong>.</p>\n<p>LiteLLM asigna a los usuarios SSO un rol por defecto que no es administrador. Pasar la\nvinculación de authentik nos metió <em>dentro</em> de la interfaz; lo que podíamos <em>hacer</em> ahí es el\nmodelo de roles propio de LiteLLM, que nunca configuramos. Autenticación, luego autorización,\nluego roles de aplicación — tres capas distintas, y superar una no dice nada de la siguiente.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"resolución-de-problemas--los-errores-que-esta-ejecución-produjo-de-verdad\">Resolución de problemas — los errores que esta ejecución produjo de verdad<a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#resoluci%C3%B3n-de-problemas--los-errores-que-esta-ejecuci%C3%B3n-produjo-de-verdad\" class=\"hash-link\" aria-label=\"Enlace directo al Resolución de problemas — los errores que esta ejecución produjo de verdad\" title=\"Enlace directo al Resolución de problemas — los errores que esta ejecución produjo de verdad\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"application-matching-query-does-not-exist\"><code>Application matching query does not exist</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#application-matching-query-does-not-exist\" class=\"hash-link\" aria-label=\"Enlace directo al application-matching-query-does-not-exist\" title=\"Enlace directo al application-matching-query-does-not-exist\" translate=\"no\">​</a></h3>\n<p>La búsqueda de <code>ak shell</code> falla aunque la aplicación se vea en la interfaz. El slug no es el\nque supusiste — authentik lo deriva del nombre, y un nombre en CamelCase como <code>LiteLLM</code> se\nconvierte en <code>lite-llm</code>. Pregunta en vez de adivinar:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.core.models import Application</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for a in Application.objects.all():</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print(repr(a.name), '-&gt;', repr(a.slug))</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-5</span><br></div></code></pre></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"redirect-uri-error\">Redirect URI Error<a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#redirect-uri-error\" class=\"hash-link\" aria-label=\"Enlace directo al Redirect URI Error\" title=\"Enlace directo al Redirect URI Error\" translate=\"no\">​</a></h3>\n<p>authentik rechaza antes de cualquier formulario. El <code>redirect_uri</code> de la barra de direcciones\nes lo que LiteLLM envió; el valor Strict del proveedor es lo que authentik espera. Los\nnuestros no coincidían porque <code>PROXY_BASE_URL</code> decía un host y la URI registrada otro. Imprime\nlo registrado y compara carácter por carácter:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.core.models import Application</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">p = Application.objects.get(slug='lite-llm').get_provider()</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for r in p.redirect_uris:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print(repr(r.matching_mode), repr(r.url))</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-5</span><br></div></code></pre></div></div>\n<p>Si lo editas y el error persiste, <strong>Applications → Clear cache</strong> — authentik cachea la\nconfiguración del proveedor.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"err_connection_refused-en-localhost4000\"><code>ERR_CONNECTION_REFUSED</code> en <code>localhost:4000</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#err_connection_refused-en-localhost4000\" class=\"hash-link\" aria-label=\"Enlace directo al err_connection_refused-en-localhost4000\" title=\"Enlace directo al err_connection_refused-en-localhost4000\" translate=\"no\">​</a></h3>\n<p>El túnel no está reenviando, aunque ssh haya conectado. Si algo ya ocupaba el puerto local,\nssh imprimió <code>bind: Address already in use</code> y siguió con un shell funcional y un reenvío\nmuerto. Comprueba el puerto y reinicia el túnel:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">lsof</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-nP</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-iTCP:4000</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sTCP:LISTEN</span><br></div></code></pre></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-usuario-que-esperaba-que-fuera-rechazado-entró\">El usuario que esperaba que fuera rechazado entró<a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#el-usuario-que-esperaba-que-fuera-rechazado-entr%C3%B3\" class=\"hash-link\" aria-label=\"Enlace directo al El usuario que esperaba que fuera rechazado entró\" title=\"Enlace directo al El usuario que esperaba que fuera rechazado entró\" translate=\"no\">​</a></h3>\n<p>Dos causas, ambas reales aquí. O una <strong>vinculación de usuario suelta</strong> le concede acceso\ndirectamente — el modo ANY significa que basta con que una regla pase — o <strong>las vinculaciones\nnunca se guardaron</strong>. Pregunta a la base de datos, no a la interfaz:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.core.models import Application</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">app = Application.objects.get(slug='lite-llm')</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">bs = list(app.bindings.all())</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('binding count:', len(bs))</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for b in bs:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print('group', b.group, '| user', b.user, '| enabled', b.enabled, '| negate', b.negate)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-5</span><br></div></code></pre></div></div>\n<p>Un usuario que llega a la <strong>pantalla de consentimiento</strong> ya pasó la comprobación de política —\nesa es la pista. Un usuario bloqueado nunca llega tan lejos.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-usuario-rechazado-entra-directo-sin-que-le-pregunten\">El usuario rechazado entra directo sin que le pregunten<a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#el-usuario-rechazado-entra-directo-sin-que-le-pregunten\" class=\"hash-link\" aria-label=\"Enlace directo al El usuario rechazado entra directo sin que le pregunten\" title=\"Enlace directo al El usuario rechazado entra directo sin que le pregunten\" translate=\"no\">​</a></h3>\n<p>Sesión del navegador, no política. Todas las ventanas de incógnito de Chrome comparten una\nsesión, así que abrir \"una ventana privada nueva\" mientras hay otra abierta mantiene al\nusuario anterior identificado. Cierra todas las ventanas de incógnito, o usa otro navegador.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"no-hay-botón-create-key-en-la-interfaz-de-administración\">No hay botón <strong>Create Key</strong> en la interfaz de administración<a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#no-hay-bot%C3%B3n-create-key-en-la-interfaz-de-administraci%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al no-hay-botón-create-key-en-la-interfaz-de-administración\" title=\"Enlace directo al no-hay-botón-create-key-en-la-interfaz-de-administración\" translate=\"no\">​</a></h3>\n<p>Has entrado por SSO, y LiteLLM da a los usuarios SSO un rol por defecto que no es\nadministrador. Crea claves desde el shell con la clave maestra, o configura los roles propios\nde LiteLLM — un mecanismo aparte de todo lo que hace authentik.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes distinguir un plano de control de un plano de datos en tu propia pila: poner SSO y\npertenencia a un grupo delante de una interfaz de administración, y luego demostrar que la API\ndetrás sigue respondiendo solo con una clave — y nombrar lo que eso significa para las bajas,\nlas claves filtradas y las claves que sobreviven a las personas.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-te-compró-esto-y-qué-no\">Qué te compró esto y qué no<a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#qu%C3%A9-te-compr%C3%B3-esto-y-qu%C3%A9-no\" class=\"hash-link\" aria-label=\"Enlace directo al Qué te compró esto y qué no\" title=\"Enlace directo al Qué te compró esto y qué no\" translate=\"no\">​</a></h2>\n<p>Hecho: un grupo que significa algo, una vinculación que lo aplica y que verificamos en la base\nde datos en lugar de fiarnos de una tabla, y una interfaz de administración donde el acceso\nsigue a la pertenencia.</p>\n<p>No hecho:</p>\n<ul>\n<li class=\"\"><strong>La API queda intacta</strong>, por diseño, como se demostró.</li>\n<li class=\"\"><strong>La clave maestra sigue entrando</strong>, saltándose todo esto.</li>\n<li class=\"\"><strong>Los usuarios SSO no tienen rol en LiteLLM</strong> más allá del de por defecto.</li>\n<li class=\"\"><strong>Sigue siendo HTTP plano.</strong> Todo token y secreto aquí cruza la red en claro.</li>\n<li class=\"\"><strong>Sin MFA.</strong></li>\n</ul>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Membership, not just an account</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-sigue\">Qué sigue<a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al Qué sigue\" title=\"Enlace directo al Qué sigue\" translate=\"no\">​</a></h2>\n<p>TLS y exposición real. El gateway está en <code>127.0.0.1:4000</code> y en este post solo es accesible a\ntravés de un túnel SSH; authentik va por HTTP plano. Hacer que cualquiera de los dos sea\naccesible de verdad significa un proxy inverso, un nombre de host y un certificado — y cada\nURI de redirección, <code>PROXY_BASE_URL</code> y emisor cambia el día que eso ocurra.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lecturas-adicionales\">Lecturas adicionales<a href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/#lecturas-adicionales\" class=\"hash-link\" aria-label=\"Enlace directo al Lecturas adicionales\" title=\"Enlace directo al Lecturas adicionales\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.goauthentik.io/docs/users-sources/groups/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Documentación de authentik — grupos</a></li>\n<li class=\"\"><a href=\"https://docs.goauthentik.io/docs/customize/policies/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Documentación de authentik — vinculaciones y el motor de políticas</a></li>\n<li class=\"\"><a href=\"https://docs.litellm.ai/docs/proxy/ui\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Documentación de LiteLLM — SSO para la interfaz de administración</a></li>\n<li class=\"\"><a href=\"https://docs.litellm.ai/docs/proxy/virtual_keys\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Documentación de LiteLLM — claves virtuales</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/",
            "title": "La vinculación que no estaba: control de acceso por grupos, y lo que el SSO no protege",
            "summary": "La Parte 3 publicó SSO con vinculaciones vacías. Cerrar ese hueco en el gateway LiteLLM destapó algo peor: el asistente de authentik nos dejó configurar las vinculaciones, las mostró en su propia tabla, y no guardó ninguna — sin error, sin aviso. Después, ya con el control de acceso funcionando, la API respondió a una petición desde un shell sin cuenta, sin sesión y sin pertenencia. Ambas cosas son el post.",
            "date_modified": "2026-09-03T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "seguridad",
                "sso",
                "oidc",
                "authentik",
                "identidad",
                "rbac",
                "litellm",
                "gateway",
                "endurecimiento"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__mcp\" src=\"data:image/png;base64,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\" alt=\"Model Context Protocol\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"487\" height=\"249\" fill=\"none\" viewBox=\"0 0 487 249\" class=\"tutorialHero__brand\"><path fill=\"#1C3C3C\" fill-rule=\"evenodd\" d=\"M124.273.412h239.031c68.111 0 123.52 55.603 123.52 123.948s-55.409 123.948-123.52 123.948H124.273c-68.11 0-123.52-55.603-123.52-123.948S56.164.412 124.274.412M234.018 192.55c3.001 3.156 7.443 3 11.378 2.182l.039.02c1.827-1.486-.77-3.368-3.249-5.165-1.487-1.077-2.932-2.125-3.356-3.038 1.373-1.674-2.687-5.474-5.847-8.433-1.327-1.242-2.495-2.335-3.037-3.061-2.248-2.448-3.152-5.537-4.062-8.644-.603-2.061-1.208-4.131-2.211-6.027-6.176-14.339-13.248-28.561-23.165-40.699-6.373-8.067-13.646-15.291-20.922-22.518-4.691-4.66-9.382-9.319-13.835-14.206-4.581-4.73-7.338-10.556-10.1-16.392-2.312-4.886-4.627-9.779-8.018-14.04-10.268-15.196-42.686-19.346-47.44 2.124.019.662-.195 1.09-.78 1.52-2.63 1.928-4.968 4.11-6.935 6.76-4.812 6.721-5.553 18.118.448 24.158l.025-.388c.2-3.05.388-5.9 2.8-8.087 4.637 3.994 11.67 5.416 17.047 2.435 6.483 9.3 8.548 20.543 10.621 31.823 1.726 9.398 3.457 18.821 7.751 27.171l.266.443c2.524 4.202 5.089 8.472 8.326 12.142 1.176 1.823 3.59 3.791 6 5.755 3.18 2.591 6.353 5.177 6.663 7.416.015.974.01 1.961.006 2.954-.025 5.879-.051 11.967 3.716 16.801 2.084 4.228-3.02 8.475-7.131 7.949-2.254.312-4.716-.282-7.161-.871-3.346-.807-6.659-1.607-9.36-.064-.758.82-1.846.849-2.94.877-1.297.035-2.601.069-3.373 1.422-.158.402-.528.856-.913 1.328-.846 1.036-1.762 2.159-.665 3.016q.149-.111.294-.224c1.663-1.269 3.248-2.479 5.493-1.724-.299 1.658.772 2.103 1.843 2.547q.281.114.553.239c-.011.385-.087.774-.163 1.159-.18.921-.356 1.824.358 2.62.339-.344.639-.732.939-1.12.735-.95 1.474-1.904 2.802-2.25 2.92 3.9 5.862 2.28 9.554.248 4.164-2.293 9.281-5.111 16.396-1.125-2.727-.136-5.163.195-6.994 2.455-.448.507-.838 1.091-.039 1.754 4.21-2.728 5.961-1.748 7.61-.825 1.19.666 2.326 1.302 4.294.493.465-.242.93-.493 1.396-.745 3.161-1.705 6.366-3.434 10.118-2.839-2.803.808-3.8 2.584-4.888 4.524-.539.959-1.099 1.957-1.911 2.898-.429.429-.624.936-.137 1.656 5.869-.488 8.087-1.976 11.083-3.986 1.429-.959 3.036-2.038 5.302-3.183 2.505-1.542 5.009-.556 7.436.4 2.633 1.037 5.175 2.037 7.527-.264.743-.7 1.674-.708 2.602-.717a10 10 0 0 0 1.002-.043c-.732-3.92-4.861-3.874-9.052-3.827-4.847.054-9.778.109-9.632-5.972 4.504-3.076 4.546-8.415 4.585-13.461.01-1.218.019-2.419.091-3.567 3.313 1.847 6.817 3.291 10.299 4.725 3.276 1.35 6.533 2.692 9.593 4.354 3.195 5.143 8.182 11.962 14.826 11.514.175-.526.331-.974.526-1.5.383.067.787.169 1.199.273 1.743.442 3.611.915 4.509-1.15m130.213-58.45a20.54 20.54 0 0 0 14.504 5.994c5.44 0 10.658-2.156 14.505-5.994a20.44 20.44 0 0 0 6.007-14.469 20.44 20.44 0 0 0-6.007-14.469 20.54 20.54 0 0 0-21.882-4.624l-11.757-17.162-8.194 5.614 11.818 17.25a20.43 20.43 0 0 0-5.002 13.391 20.43 20.43 0 0 0 6.008 14.469m-36.808-55.576a20.55 20.55 0 0 0 21.408-1.964 20.46 20.46 0 0 0 7.34-10.483 20.4 20.4 0 0 0-.331-12.784 20.47 20.47 0 0 0-7.873-10.09 20.557 20.557 0 0 0-18.35-2.282 20.5 20.5 0 0 0-7.964 5.175 20.44 20.44 0 0 0-4.77 8.201 20.429 20.429 0 0 0 3.232 18.164 20.5 20.5 0 0 0 7.308 6.063m0 118.824a20.55 20.55 0 0 0 21.408-1.964 20.46 20.46 0 0 0 7.34-10.483 20.4 20.4 0 0 0-.331-12.783 20.47 20.47 0 0 0-7.873-10.092 20.55 20.55 0 0 0-26.314 2.894 20.45 20.45 0 0 0-4.77 8.201 20.43 20.43 0 0 0 3.232 18.164 20.5 20.5 0 0 0 7.308 6.063m18.857-72.629v-10.174h-31.394a20.3 20.3 0 0 0-4.398-8.342L322.3 88.704l-8.59-5.698-11.812 17.499a20.5 20.5 0 0 0-6.749-1.221 20.5 20.5 0 0 0-14.462 5.959 20.3 20.3 0 0 0-5.99 14.388 20.3 20.3 0 0 0 5.99 14.387 20.5 20.5 0 0 0 14.462 5.96 20.5 20.5 0 0 0 6.749-1.221l11.812 17.498 8.487-5.697-11.709-17.498a20.3 20.3 0 0 0 4.398-8.342z\" clip-rule=\"evenodd\"></path></svg><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/litellm-wordmark-86d6f0432720b27534e05bc579dfc29c.png\" alt=\"LiteLLM\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 4 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->4</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->4<!-- --> earned</span></div><div class=\"skillTracker__series\">Agent orchestration with LangGraph</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">State that survives a restart</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Hand work between agents</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">3</span><span class=\"skillTracker__skill\" data-state=\"current\">Governed tools an agent can call</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Engineer the context, not the prompt</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>La <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/\">Parte 1</a> le dio a un agente un estado que\nsobrevive a un reinicio, y una pausa donde un humano aprueba antes de que continúe. La\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/\">Parte 2</a> puso un supervisor delante de un\nagente de <strong>agenda</strong> y uno de <strong>facturación</strong>, y repartió el trabajo entre ellos.</p>\n<p>Ninguno de esos agentes agendó ni facturó nada. Lo comentaron. Cada herramienta que tenían era\nuna función que tú escribiste en el mismo proceso, y las interesantes — buscar un cliente,\nocupar un hueco, mover dinero — no existían.</p>\n<p>Esta parte construye la caja de herramientas que esos dos roles necesitan, por <strong>MCP</strong>: un\ncalendario, una lista de clientes, facturas y un reembolso. Para reducir piezas móviles\napuntamos <strong>un</strong> agente a las cinco herramientas en lugar de reconstruir el supervisor de la\nParte 2 — el agente las descubre en tiempo de ejecución en vez de estar cableado a ellas, las\nencadena para responder una pregunta, y escribe filas que puedes ir a comprobar en la base de\ndatos después. Luego el reembolso lo detiene en seco y hace que un humano teclee el importe.</p>\n<p>Repartir estas herramientas de vuelta entre el agente de agenda y el de facturación — de modo\nque el de agenda <em>no pueda ver</em> <code>issue_refund</code> en absoluto — es el siguiente paso natural, y la\núltima sección explica cómo.</p>\n<p>En julio la especificación de MCP <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/news/mcp-2026-07-28-spec/\">eliminó las sesiones por completo</a>.\nEl día que ejecutamos esto, LangChain publicó el soporte para esa revisión en el paquete\nprincipal. Así que esta es una primera ejecución contra un protocolo de cinco semanas y un cliente\ndel mismo día — que es justo por lo que tres cosas de aquí no están todavía en la documentación\nde nadie.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>Una WEC Instance: 8 vCPU AMD EPYC 7601, 15 GB RAM, Ubuntu 22.04, Python 3.10. <code>langchain</code>\n1.4.0, <code>langchain-openai</code> 1.6.0, <code>langgraph</code> 1.2.11, <code>mcp</code> 2.1.1, <code>fastmcp</code> 4.0.2 — todo\ninstalado el día de escribir esto. Modelos servidos a través del gateway LiteLLM de la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/\">Parte 1 de la serie del gateway</a>, que reenvía a\nWEC Inference. <code>langchain.mcp</code> está en beta y avisa en cada importación; la API puede moverse.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-es-mcp-en-realidad\">Qué es MCP en realidad<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/#qu%C3%A9-es-mcp-en-realidad\" class=\"hash-link\" aria-label=\"Enlace directo al Qué es MCP en realidad\" title=\"Enlace directo al Qué es MCP en realidad\" translate=\"no\">​</a></h2>\n<p>Un servidor publica herramientas. Un cliente le pregunta qué herramientas existen. El modelo\nelige una y el cliente la llama. Ese es todo el protocolo — el valor está en que \"preguntar qué\nherramientas existen\" esté estandarizado, así que cualquier cliente puede usar cualquier\nservidor sin pegamento escrito para ese par concreto.</p>\n<p>Tres palabras que necesitas:</p>\n<p><strong>Herramienta.</strong> Una función que el servidor expone, con una descripción y un esquema JSON para\nsus argumentos. La descripción es lo que el modelo lee para decidir si la llama; el esquema es\nlo que debe rellenar. Nada más de tu código llega al modelo.</p>\n<p><strong>Descubrimiento.</strong> El cliente pide <code>tools/list</code> y recibe ese catálogo. Tu agente aprende lo que\npuede hacer en tiempo de ejecución en lugar de cuando lo escribiste.</p>\n<p><strong>Elicitación.</strong> Una herramienta que no puede terminar sin preguntarle algo a un humano —\nconfirmar un borrado, aportar un parámetro que el modelo no tenía. Con la nueva especificación\nesto es una petición corriente que el cliente reintenta con la respuesta adjunta.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-viaje-de-ida-y-vuelta-que-sustituyó-a-la-sesión\">El viaje de ida y vuelta que sustituyó a la sesión<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/#el-viaje-de-ida-y-vuelta-que-sustituy%C3%B3-a-la-sesi%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al El viaje de ida y vuelta que sustituyó a la sesión\" title=\"Enlace directo al El viaje de ida y vuelta que sustituyó a la sesión\" translate=\"no\">​</a></h2>\n<p>Con la especificación antigua, una herramienta que necesitaba datos mantenía la conexión\nabierta y preguntaba por un canal de retorno. Con <code>2026-07-28</code> retorna, y el cliente la llama\n<strong>otra vez</strong> con la respuesta adjunta. Dos peticiones completas en lugar de una de larga\nduración:</p>\n<!-- -->\n<p>La herramienta se ejecuta <strong>dos veces</strong>. Por eso su cuerpo empieza con\n<code>if ctx.input_responses is None</code> — tiene que estar escrita para ser reentrada, no reanudada.\nNada queda abierto entre medias, así que la respuesta puede llegar después de un redespliegue, o\na otra réplica detrás de un balanceador.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">El entorno virtual y el acceso a WEC Inference de la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/\">Parte 1</a></li>\n<li class=\"\">Una <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/\">gateway LiteLLM</a> con una clave virtual, o\ncualquier endpoint compatible con OpenAI al que puedas apuntar un modelo</li>\n<li class=\"\">Python 3.10+</li>\n<li class=\"\">Dos sesiones de terminal en la caja — el servidor bloquea una de ellas</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--instalar\">Paso 1 — Instalar<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/#paso-1--instalar\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Instalar\" title=\"Enlace directo al Paso 1 — Instalar\" translate=\"no\">​</a></h2>\n<p>El soporte de MCP ahora vive en el paquete <code>langchain</code> principal. Si tienes\n<code>langchain-mcp-adapters</code> de antes, es justo lo que esto sustituye.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">mkdir</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> venv .venv </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/pip </span><span class=\"token function\" style=\"color:#d73a49\">install</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-q</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"langchain[mcp]\"</span><span class=\"token plain\"> langchain-openai httpx </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/pip list </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-iE</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'langchain|langgraph|^mcp |fastmcp'</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"pip list mostrando langchain 1.4.0, langgraph 1.2.11, mcp 2.1.1 y fastmcp 4.0.2\" src=\"https://development-wec.wiline.com/docs/es/assets/images/mcp-versions-5b3443d87f1bcddd968257037aceb26f.png\" width=\"749\" height=\"395\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><code>langchain[mcp]</code> necesita <strong>1.4.0 o superior</strong> — por debajo, <code>MCPAdapter</code> no existe.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span><code>mcp[cli]</code> ya no existe</div><div class=\"admonitionContent_BuS1\"><p>Las guías antiguas te dicen que instales <code>\"mcp[cli]\"</code>. En <code>mcp</code> 2.x ese extra ya no existe, y\npip falla con un muro de <code>does not provide the extra 'cli'</code> a lo largo de sesenta versiones\nantes de rendirse con <code>ResolutionImpossible</code>. Parece un conflicto de dependencias; es un extra\neliminado.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--algo-sobre-lo-que-las-herramientas-actúen\">Paso 2 — Algo sobre lo que las herramientas actúen<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/#paso-2--algo-sobre-lo-que-las-herramientas-act%C3%BAen\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — Algo sobre lo que las herramientas actúen\" title=\"Enlace directo al Paso 2 — Algo sobre lo que las herramientas actúen\" translate=\"no\">​</a></h2>\n<p>Los agentes necesitan datos que existan. SQLite, sembrado una vez — tres clientes, seis huecos\n(uno ya ocupado), cuatro facturas.</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/mcp-tools/seed.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> sqlite3</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">db </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> sqlite3</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">connect</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"office.db\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">db</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">executescript</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">DROP TABLE IF EXISTS customers; DROP TABLE IF EXISTS slots; DROP TABLE IF EXISTS invoices;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">CREATE TABLE customers(id INTEGER PRIMARY KEY, name TEXT, email TEXT);</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">CREATE TABLE slots(id INTEGER PRIMARY KEY, day TEXT, time TEXT, customer_id INTEGER);</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">CREATE TABLE invoices(id INTEGER PRIMARY KEY, customer_id INTEGER, amount REAL, status TEXT);</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">INSERT INTO customers(name,email) VALUES</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\"> ('Maria Alvarez','maria@example.com'),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\"> ('Tomas Reis','tomas@example.com'),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\"> ('Priya Nair','priya@example.com');</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">INSERT INTO slots(day,time,customer_id) VALUES</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\"> ('2026-09-04','09:00',NULL),('2026-09-04','11:00',NULL),('2026-09-04','14:00',2),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\"> ('2026-09-05','09:00',NULL),('2026-09-05','11:00',NULL),('2026-09-05','16:00',NULL);</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">INSERT INTO invoices(customer_id,amount,status) VALUES</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\"> (1,240.00,'paid'),(1,80.00,'open'),(2,150.00,'paid'),(3,410.00,'open');</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">db</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">commit</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Dos detalles cargan peso. <strong>Maria tiene una factura pagada y otra abierta</strong>, así que \"reembolsa\nla factura de Maria\" es ambiguo y el agente tiene que resolverlo. <strong>Tomas ya ocupa el viernes a\nlas 14:00</strong>, así que la disponibilidad es una pregunta real y no \"todo\".</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--la-caja-de-herramientas\">Paso 3 — La caja de herramientas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/#paso-3--la-caja-de-herramientas\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — La caja de herramientas\" title=\"Enlace directo al Paso 3 — La caja de herramientas\" translate=\"no\">​</a></h2>\n<p>Cinco herramientas: tres de lectura, una de escritura, y una que no puede terminar sin un\nhumano.</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/mcp-tools/office_tools.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> sqlite3</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastmcp </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> FastMCP</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> Context</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> mcp</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">types </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> InputRequiredResult</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> ElicitRequest</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> ElicitRequestFormParams</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">DB </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">path</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">join</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">path</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">dirname</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">path</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">abspath</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">__file__</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"office.db\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">mcp </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> FastMCP</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"office-tools\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">q</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sql</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> args</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    con </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> sqlite3</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">connect</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">DB</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> con</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">row_factory </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> sqlite3</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">Row</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">try</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        rows </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> con</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">execute</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sql</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> args</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">fetchall</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> con</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">commit</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">r</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> r </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> rows</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">finally</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        con</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">close</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">w</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sql</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> args</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    con </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> sqlite3</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">connect</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">DB</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">try</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        cur </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> con</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">execute</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sql</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> args</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> con</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">commit</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> cur</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">rowcount</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">finally</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        con</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">close</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@mcp</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">tool</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">find_customer</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">query</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"Find customers whose name or email matches the query.\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    like </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"%</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">query</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">%\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> q</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"SELECT id, name, email FROM customers WHERE name LIKE ? OR email LIKE ?\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">like</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> like</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@mcp</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">tool</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">list_availability</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">day</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"List free appointment slots on a given day, formatted YYYY-MM-DD.\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> q</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"SELECT id, day, time FROM slots WHERE day = ? AND customer_id IS NULL ORDER BY time\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">day</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@mcp</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">tool</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">book_slot</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">slot_id</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">int</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> customer_id</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">int</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"Book a free slot for a customer. Fails if the slot is already taken.\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    n </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> w</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"UPDATE slots SET customer_id = ? WHERE id = ? AND customer_id IS NULL\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">customer_id</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> slot_id</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> n </span><span class=\"token operator\" style=\"color:#393A34\">==</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Slot </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">slot_id</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> is already taken or does not exist.\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    row </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> q</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"SELECT day, time FROM slots WHERE id = ?\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">slot_id</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Booked slot </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">slot_id</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> (</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">row</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'day'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">row</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'time'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">) for customer </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">customer_id</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">.\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@mcp</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">tool</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">open_invoices</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">customer_id</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">int</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"List a customer's invoices with their amounts and status.\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> q</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"SELECT id, amount, status FROM invoices WHERE customer_id = ?\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">customer_id</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@mcp</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">tool</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">issue_refund</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">invoice_id</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">int</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> ctx</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Context</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> InputRequiredResult</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"Refund an invoice. A human must approve the amount and give a reason.\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    inv </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> q</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"SELECT id, customer_id, amount, status FROM invoices WHERE id = ?\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">invoice_id</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> inv</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"No invoice </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">invoice_id</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">.\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    inv </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> inv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    responses </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ctx</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">input_responses</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> responses </span><span class=\"token keyword\" style=\"color:#00009f\">is</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        params </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ElicitRequestFormParams</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            message</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Refund invoice </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">inv</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'id'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> for customer </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">inv</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'customer_id'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">? \"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"It is </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">inv</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'status'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">, amount </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">inv</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'amount'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">:</span><span class=\"token string-interpolation interpolation format-spec\">.2f</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">. \"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string\" style=\"color:#e3116c\">\"Confirm the amount to refund and give a reason.\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            requested_schema</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"object\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string\" style=\"color:#e3116c\">\"properties\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                    </span><span class=\"token string\" style=\"color:#e3116c\">\"amount\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"number\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"description\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Amount to refund\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                    </span><span class=\"token string\" style=\"color:#e3116c\">\"reason\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"string\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"description\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Why this refund is being issued\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string\" style=\"color:#e3116c\">\"required\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"amount\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"reason\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> InputRequiredResult</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            result_type</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"input_required\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            input_requests</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"approve_refund\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> ElicitRequest</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">method</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"elicitation/create\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> params</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">params</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    answer </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> responses</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"approve_refund\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> answer</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">action </span><span class=\"token operator\" style=\"color:#393A34\">!=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"accept\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Refund declined - invoice </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">inv</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'id'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> unchanged.\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    w</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"UPDATE invoices SET status = 'refunded' WHERE id = ?\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">inv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Refunded </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">answer</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">content</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'amount'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">:</span><span class=\"token string-interpolation interpolation format-spec\">.2f</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> on invoice </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">inv</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'id'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">. Reason: </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">answer</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">content</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'reason'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> __name__ </span><span class=\"token operator\" style=\"color:#393A34\">==</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"__main__\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    mcp</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">transport</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"http\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> host</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"127.0.0.1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> port</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">8770</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> stateless_http</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Los docstrings no son comentarios. Se convierten en las descripciones que el modelo lee para\nelegir, así que escríbelos como instrucciones para alguien que no puede ver más que esa línea.\nLos dejamos en inglés porque son el texto que el modelo consume.</p>\n<p><code>book_slot</code> rechaza un hueco ocupado en SQL en vez de comprobarlo antes — <code>WHERE customer_id IS NULL</code> significa que dos agentes compitiendo por el mismo hueco no pueden ganar ambos.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python office_tools.py</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--la-sesión-que-no-debería-existir\">Paso 4 — La sesión que no debería existir<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/#paso-4--la-sesi%C3%B3n-que-no-deber%C3%ADa-existir\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — La sesión que no debería existir\" title=\"Enlace directo al Paso 4 — La sesión que no debería existir\" translate=\"no\">​</a></h2>\n<p>Déjalo corriendo. En una segunda sesión, pregúntale qué herramientas tiene:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST http://127.0.0.1:8770/mcp </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Accept: application/json, text/event-stream'</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'</span><br></div></code></pre></div></div>\n<p>La primera vez que ejecutamos esto, sin <code>stateless_http=True</code>:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"jsonrpc\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token string\" style=\"color:#e3116c\">\"2.0\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token property\" style=\"color:#36acaa\">\"id\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token null keyword\" style=\"color:#00009f\">null</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token property\" style=\"color:#36acaa\">\"error\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"code\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">-32600</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token property\" style=\"color:#36acaa\">\"message\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token string\" style=\"color:#e3116c\">\"Bad Request: Missing session ID\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El endpoint MCP rechazando una petición con Bad Request: Missing session ID\" src=\"https://development-wec.wiline.com/docs/es/assets/images/mcp-session-error-7897aebee33998b591bd27b4606a6e6b.png\" width=\"666\" height=\"75\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-danger admonition_xJq3 alert alert--danger\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z\"></path></svg></span>La especificación es sin estado. Tu servidor no, por defecto.</div><div class=\"admonitionContent_BuS1\"><p>La revisión <code>2026-07-28</code> eliminó las sesiones del protocolo — sin handshake, sin ID de sesión,\nnada que fije un cliente a una instancia. Todo lo escrito sobre el cambio describe bien el lado\ncliente: no queda nada que fijar.</p><p><strong>FastMCP 4.0.2 sigue usando el transporte con estado por defecto.</strong> Optas por el nuevo\ncomportamiento con <code>stateless_http=True</code>, y hasta que lo hagas, la primera petición contra tu\nservidor recién creado falla citando un concepto que la especificación borró hace cinco semanas.</p><p><code>run_http_async</code> acepta tanto <code>stateless_http</code> como <code>stateless</code>; el docstring dice que el\nsegundo es \"Alias for stateless_http for CLI consistency\". Un interruptor, dos nombres, ambos\ncon valor por defecto <code>None</code>.</p></div></div>\n<p>La línea de arranque es donde lo compruebas:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"FastMCP arrancando sin el marcador stateless\" src=\"https://development-wec.wiline.com/docs/es/assets/images/mcp-server-start-9b984393ff73ca52793743fe31bbe9da.png\" width=\"756\" height=\"488\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"FastMCP arrancando con transport http (stateless)\" src=\"https://development-wec.wiline.com/docs/es/assets/images/mcp-server-stateless-8cd7f1d5201b5dc974f4726c52b23f9f.png\" width=\"776\" height=\"482\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Con la bandera, la misma petición devuelve el catálogo — sin handshake, sin sesión:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Las cinco herramientas de oficina listadas con sus descripciones\" src=\"https://development-wec.wiline.com/docs/es/assets/images/mcp-tools-list-c1f510e50217be4d48c633e07dafa45d.png\" width=\"744\" height=\"180\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>El esquema de <code>issue_refund</code> contiene solo <code>invoice_id</code> — el parámetro <code>ctx: Context</code> no está,\nporque FastMCP lo inyecta y el modelo no puede fijarlo. Eso es lo que hace que una confirmación\nobligatoria sea obligatoria.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5--un-agente-que-descubre-sus-herramientas\">Paso 5 — Un agente que descubre sus herramientas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/#paso-5--un-agente-que-descubre-sus-herramientas\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 5 — Un agente que descubre sus herramientas\" title=\"Enlace directo al Paso 5 — Un agente que descubre sus herramientas\" translate=\"no\">​</a></h2>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/mcp-tools/agent.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> sys</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">agents </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> create_agent</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">mcp </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> MCPAdapter</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain_openai </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> ChatOpenAI</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">model </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ChatOpenAI</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    base_url</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"http://127.0.0.1:4000/v1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    api_key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"GATEWAY_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    model</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"qwen-mid\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    temperature</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> MCPAdapter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"http://127.0.0.1:8770/mcp\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> adapter</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        tools </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> adapter</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">list_tools</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"discovered:\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">t</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">name </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> t </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> tools</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        agent </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> create_agent</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">model</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> tools</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        out </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> agent</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">ainvoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"role\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"user\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"\\n--- answer ---\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">out</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Todos los ejemplos publicados de MCP apuntan el modelo a Anthropic, OpenAI o Gemini. Este apunta\na tu propia <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/\">gateway LiteLLM</a>, que reenvía a WEC\nInference — las herramientas son locales, el modelo es tuyo, nada sale de la caja.</p>\n<p>Guarda la clave en un archivo y no en la línea de comandos, donde acabaría en tu historial de\nshell y en cada captura de pantalla:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">printf</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'GATEWAY_KEY=%s\\n'</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">chmod</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">600</span><span class=\"token plain\"> .env</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-a</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">source</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> +a </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python agent.py </span><span class=\"token string\" style=\"color:#e3116c\">\"Who is Maria and what invoices does she have?\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El agente encadenando find_customer con open_invoices e informando de las dos facturas de Maria\" src=\"https://development-wec.wiline.com/docs/es/assets/images/mcp-agent-lookup-d1e719855fca0eb978d89518c2a03cac.png\" width=\"983\" height=\"209\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Esa respuesta necesitó <strong>dos llamadas a herramientas en secuencia</strong>: <code>find_customer(\"Maria\")</code>\npara obtener su id, y luego <code>open_invoices(1)</code> usándolo. Nada encaminó eso — el modelo leyó\ncinco descripciones y dedujo el orden.</p>\n<p>Ahora una que escribe:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python agent.py </span><span class=\"token string\" style=\"color:#e3116c\">\"Book Maria into a free slot on 2026-09-04\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El agente encontrando a Maria, listando los huecos libres del viernes y reservando las 09:00\" src=\"https://development-wec.wiline.com/docs/es/assets/images/mcp-agent-booking-cf43e46e8fbeb7003efd7d2ceba1ddc4.png\" width=\"982\" height=\"157\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Tres llamadas esta vez. Y como una reserva es un cambio real, compruébalo donde el agente no\nllega:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">import sqlite3</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for r in sqlite3.connect('office.db').execute('select s.id,s.day,s.time,coalesce(c.name,</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">free</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">) from slots s left join customers c on c.id=s.customer_id order by s.day,s.time'): print(r)\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La tabla de huecos mostrando a Maria reservada en 2026-09-04 09:00\" src=\"https://development-wec.wiline.com/docs/es/assets/images/mcp-verify-db-e3eda545965f9b86a42918a3fd4bc674.png\" width=\"992\" height=\"173\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes levantar un servidor MCP sobre un almacén de datos real, apuntarle un agente, y hacer que\nencadene herramientas descubiertas hasta producir un cambio que verificas en la base de datos —\ncon el modelo en tu propio gateway y no en la de un proveedor.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-6--la-herramienta-que-se-niega-a-terminar-sola\">Paso 6 — La herramienta que se niega a terminar sola<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/#paso-6--la-herramienta-que-se-niega-a-terminar-sola\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 6 — La herramienta que se niega a terminar sola\" title=\"Enlace directo al Paso 6 — La herramienta que se niega a terminar sola\" translate=\"no\">​</a></h2>\n<p>La pausa de aprobación de la Parte 1 era algo que <em>tú</em> pusiste en el grafo. Esta viene de la\nherramienta, y el agente no tiene forma de saltársela.</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/mcp-tools/elicit.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> json</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> os</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">agents </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> create_agent</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">mcp </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> MCPAdapter</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain_openai </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> ChatOpenAI</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">checkpoint</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">memory </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> InMemorySaver</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">types </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Command</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">model </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ChatOpenAI</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">base_url</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"http://127.0.0.1:4000/v1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                   api_key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"GATEWAY_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> model</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"qwen-mid\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> temperature</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">ask_human</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">req</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"\\n&gt;&gt;&gt; </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">req</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'message'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    props </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">req</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"requested_schema\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"properties\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    content </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> name</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> spec </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> props</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">items</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        raw </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token builtin\">input</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"    </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">name</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> (</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">spec</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">get</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'type'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">,</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'string'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">): \"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">strip</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        content</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token builtin\">float</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">raw</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> spec</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">==</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"number\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> raw</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"action\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"accept\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> content</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> MCPAdapter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"http://127.0.0.1:8770/mcp\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> adapter</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        tools </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> adapter</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">list_tools</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        agent </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> create_agent</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">model</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> tools</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> checkpointer</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">InMemorySaver</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        cfg </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"configurable\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"thread_id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"refund-1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        out </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> agent</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">ainvoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"role\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"user\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Refund Maria's open invoice.\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> cfg</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"__interrupt__\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> out</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"NO INTERRUPT:\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> out</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        payload </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> out</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"__interrupt__\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">value</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"=== INTERRUPT ===\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">json</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">dumps</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">payload</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> indent</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        responses </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">r</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"key\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> ask_human</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">r</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> r </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> payload</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"requests\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"\\n=== RESUMING ===\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        out </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> agent</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">ainvoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">Command</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">resume</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"responses\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> responses</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> cfg</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">out</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>El <code>checkpointer</code> y el <code>thread_id</code> no son opcionales — una ejecución interrumpida necesita algún\nsitio donde esperar. El mismo requisito que la pausa de aprobación de la Parte 1.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La carga del interrupt, el importe y el motivo tecleados, y el resultado tras reanudar\" src=\"https://development-wec.wiline.com/docs/es/assets/images/mcp-elicit-full-b00707c0653a77cd26dcf4221b184fa5.png\" width=\"1044\" height=\"732\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Lee el orden de los hechos. El modelo resolvió <em>\"la factura abierta de Maria\"</em> como la factura 2\npor sí solo — ella tiene dos, y eligió la abierta. Luego se detuvo. <strong>El importe y el motivo los\ntecleó un humano</strong>, y solo entonces ocurrió el reembolso.</p>\n<p>La carga es <code>{\"type\": \"mcp_elicitation\", \"tool_name\": \"issue_refund\", \"requests\": [...]}</code>. <code>type</code>\nes el discriminador, de modo que un grafo con varios tipos de interrupt puede saber cuál vino de\nMCP. Cada petición lleva una <code>key</code>, un <code>message</code> para un humano, un <code>mode</code> de <code>form</code> o <code>url</code>, y\npara un formulario el <code>requested_schema</code> que la respuesta debe satisfacer — aquí dos campos\nobligatorios, uno de ellos numérico. Reanudas con una respuesta por clave.</p>\n<p>Y verifica, otra vez fuera del agente:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La tabla de facturas mostrando la factura 2 como refunded\" src=\"https://development-wec.wiline.com/docs/es/assets/images/mcp-verify-refund-3f9c62f71c37c5e0339cf4d1a4da57df.png\" width=\"1043\" height=\"356\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes hacer que una herramienta se niegue a terminar sin un humano — devolviendo un formulario\ntipado que el modelo no puede rellenar por su cuenta, pausando la ejecución como un interrupt de\nLangGraph, y reanudándola con una respuesta que una persona tecleó de verdad.</p></div></div>\n<div class=\"theme-admonition theme-admonition-danger admonition_xJq3 alert alert--danger\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z\"></path></svg></span><code>ctx.elicit</code> es la API vieja, y su error aterriza en el contexto del modelo</div><div class=\"admonitionContent_BuS1\"><p>Todos los ejemplos de elicitación de FastMCP que encontrarás llaman a\n<code>await ctx.elicit(message, response_type)</code> dentro de la herramienta. Ese es el mecanismo <strong>de la\nera del handshake</strong>: bloquea a mitad de ejecución y habla por el canal de retorno de la sesión —\nel canal de retorno que el transporte sin estado no tiene.</p><p>Nuestro primer intento lo usó. El agente no se pausó — pero no porque no se lanzara nada.\nFastMCP lanzó su error de era exactamente como está documentado, LangChain lo convirtió en un\n<code>ToolMessage</code> con <code>status=\"error\"</code> (deliberadamente, \"en lugar de terminar la ejecución\"), y el\nmodelo leyó ese error y lo narró como progreso:</p><p><span class=\"zoomImage__wrap\"><img alt=\"La ejecución imprimiendo interrupt raised? False, issue_refund con status=error llevando el error de era, el modelo respondiendo que ha iniciado el reembolso y que un humano debe aprobarlo, y sqlite3 mostrando la factura 2 todavía abierta\" src=\"https://development-wec.wiline.com/docs/es/assets/images/mcp-elicit-old-api-ec02d789aa6970b70a130f83067c235b.png\" width=\"1038\" height=\"437\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p><p>Sin interrupt, código de salida 0, y la factura sigue <code>open</code>. No se reembolsó nada y nada está\nesperando a un humano — pero al usuario se le dijo que hay una aprobación pendiente.</p><p>Con el protocolo moderno una herramienta <strong>devuelve</strong> un <code>InputRequiredResult</code> describiendo lo\nque necesita, y sale. El cliente responde y <strong>reintenta la llamada completa</strong> con la respuesta\nadjunta. Cada ronda es una petición completa, y por eso sobrevive al balanceo de carga y a los\nredespliegues — y por eso el cuerpo de la herramienta debe escribirse para ser reentrado, no\nreanudado. El nuestro lo hace con <code>if ctx.input_responses is None</code>.</p><p>La documentación de FastMCP es explícita: <code>ctx.elicit</code> solo funciona en conexiones\n<code>≤ 2025-11-25</code>, y un servidor multi-era debería ramificar según\n<code>ctx.request_context.protocol_version</code>. También promete que el desajuste \"lanza un error de era\nclaro en lugar de fallar de forma oscura\" — y así es. El error simplemente aterriza en el\ncontexto del modelo y no en el tuyo.</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>El fragmento del anuncio no funciona</div><div class=\"admonitionContent_BuS1\"><p>El post de LangChain lee el interrupt como <code>paused[\"__interrupt__\"][0].value.requests[0]</code> —\nacceso por atributo. <code>MCPElicitationInterrupt</code> es un <code>TypedDict</code> (ver\n<code>langchain/mcp/elicitation.py</code> en 1.4.0), así que en tiempo de ejecución es un dict y\n<code>.requests</code> no resuelve. Usa <code>value[\"requests\"]</code> — como hace el propio fragmento unas líneas\ndespués, leyendo <code>question[\"key\"]</code>.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-7--la-caché-y-por-qué-aquí-no-hace-nada\">Paso 7 — La caché, y por qué aquí no hace nada<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/#paso-7--la-cach%C3%A9-y-por-qu%C3%A9-aqu%C3%AD-no-hace-nada\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 7 — La caché, y por qué aquí no hace nada\" title=\"Enlace directo al Paso 7 — La caché, y por qué aquí no hace nada\" translate=\"no\">​</a></h2>\n<p>Cada ejecución empieza descubriendo herramientas, lo que es un viaje de ida y vuelta antes de\nque el modelo vea nada. La nueva especificación lo hace cacheable:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/mcp-tools/cached.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> time</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastmcp </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Client</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">mcp </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> MCPAdapter</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    client </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> Client</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"http://127.0.0.1:8770/mcp\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> cache</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> MCPAdapter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">client</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> adapter</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> i </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            t </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">perf_counter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            tools </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> adapter</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">list_tools</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">cache_mode</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"use\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"discovery </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">i</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">: </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation builtin\">len</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation\">tools</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> tools in </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation\">time</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">perf_counter</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation operator\" style=\"color:#393A34\">-</span><span class=\"token string-interpolation interpolation\">t</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation operator\" style=\"color:#393A34\">*</span><span class=\"token string-interpolation interpolation number\" style=\"color:#36acaa\">1000</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">:</span><span class=\"token string-interpolation interpolation format-spec\">.1f</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> ms\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">discovery 1: 5 tools in 14.6 ms</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">discovery 2: 5 tools in 12.5 ms</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Dos descubrimientos de cinco herramientas cronometrados en 14,6 ms y 12,5 ms, sin efecto de caché\" src=\"https://development-wec.wiline.com/docs/es/assets/images/mcp-cache-bbadf88f6b26a069ec15e3b3db635c02.png\" width=\"801\" height=\"125\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Sin efecto — y ese es el comportamiento correcto, no una caché rota.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span><code>cache=True</code> es necesario, no suficiente</div><div class=\"admonitionContent_BuS1\"><p>La caché del cliente \"respeta las pistas <code>ttlMs</code> y <code>cacheScope</code> que el servidor adjunta a cada\nrespuesta\" y solo funciona contra servidores de era moderna que las anuncien. El nuestro no\nanuncia ninguna, así que ambas llamadas van a la red.</p><p>Fíjate además en que <code>cache=True</code> va en el <strong><code>fastmcp.Client</code></strong>, no en <code>MCPAdapter</code> — el\nadaptador acepta exactamente un argumento. Y la caché pertenece al cliente, así que un cliente\npor llamante evita que los catálogos se crucen entre inquilinos.</p></div></div>\n<p>Por loopback no había nada que ahorrar de todos modos: 12–15 ms es el viaje de ida y vuelta. La\ncaché es para servidores remotos con latencia real y un TTL que respetar.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"resolución-de-problemas--los-errores-que-esta-ejecución-produjo-de-verdad\">Resolución de problemas — los errores que esta ejecución produjo de verdad<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/#resoluci%C3%B3n-de-problemas--los-errores-que-esta-ejecuci%C3%B3n-produjo-de-verdad\" class=\"hash-link\" aria-label=\"Enlace directo al Resolución de problemas — los errores que esta ejecución produjo de verdad\" title=\"Enlace directo al Resolución de problemas — los errores que esta ejecución produjo de verdad\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"resolutionimpossible-al-instalar-mcpcli\"><code>ResolutionImpossible</code> al instalar <code>mcp[cli]</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/#resolutionimpossible-al-instalar-mcpcli\" class=\"hash-link\" aria-label=\"Enlace directo al resolutionimpossible-al-instalar-mcpcli\" title=\"Enlace directo al resolutionimpossible-al-instalar-mcpcli\" translate=\"no\">​</a></h3>\n<p>El extra <code>cli</code> se eliminó en <code>mcp</code> 2.x. Instala <code>\"langchain[mcp]\"</code> en su lugar; arrastra un\n<code>mcp</code> y un <code>fastmcp</code> compatibles.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"bad-request-missing-session-id\"><code>Bad Request: Missing session ID</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/#bad-request-missing-session-id\" class=\"hash-link\" aria-label=\"Enlace directo al bad-request-missing-session-id\" title=\"Enlace directo al bad-request-missing-session-id\" translate=\"no\">​</a></h3>\n<p>Tu servidor está usando el transporte con estado. Añade <code>stateless_http=True</code> a <code>mcp.run(...)</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"keyerror-gateway_key\"><code>KeyError: 'GATEWAY_KEY'</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/#keyerror-gateway_key\" class=\"hash-link\" aria-label=\"Enlace directo al keyerror-gateway_key\" title=\"Enlace directo al keyerror-gateway_key\" translate=\"no\">​</a></h3>\n<p><code>source .env</code> define variables de shell, no variables de entorno — el proceso hijo de Python\nnunca las ve. Usa <code>set -a &amp;&amp; source .env &amp;&amp; set +a</code>. Y <code>export</code> no cruza entre sesiones SSH, así\nque una segunda terminal necesita su propio <code>source</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"address-already-in-use-tras-reiniciar-el-servidor\"><code>address already in use</code> tras reiniciar el servidor<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/#address-already-in-use-tras-reiniciar-el-servidor\" class=\"hash-link\" aria-label=\"Enlace directo al address-already-in-use-tras-reiniciar-el-servidor\" title=\"Enlace directo al address-already-in-use-tras-reiniciar-el-servidor\" translate=\"no\">​</a></h3>\n<p>La instancia anterior sigue ocupando el puerto — un reinicio fallido deja vivo el proceso\nantiguo, así que el arreglo que acabas de hacer parece no funcionar. <code>pkill -f office_tools.py</code>,\ny luego comprueba con <code>ss -tlnp | grep 8770</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-agente-nunca-se-pausa-en-una-herramienta-destructiva\">El agente nunca se pausa en una herramienta destructiva<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/#el-agente-nunca-se-pausa-en-una-herramienta-destructiva\" class=\"hash-link\" aria-label=\"Enlace directo al El agente nunca se pausa en una herramienta destructiva\" title=\"Enlace directo al El agente nunca se pausa en una herramienta destructiva\" translate=\"no\">​</a></h3>\n<p>La herramienta está llamando a <code>ctx.elicit</code> en lugar de devolver <code>InputRequiredResult</code>. En una\nconexión sin estado no hay canal de retorno, y la llamada no lanza el error donde lo esperarías.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-te-compró-esto-y-qué-no\">Qué te compró esto y qué no<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/#qu%C3%A9-te-compr%C3%B3-esto-y-qu%C3%A9-no\" class=\"hash-link\" aria-label=\"Enlace directo al Qué te compró esto y qué no\" title=\"Enlace directo al Qué te compró esto y qué no\" translate=\"no\">​</a></h2>\n<p>Hecho: un servidor MCP sobre un almacén de datos real, un agente que descubre herramientas en\nlugar de estar cableado a ellas y las encadena sin encaminamiento, dos cambios que verificaste en\nla base de datos en vez de creerte al modelo, y un reembolso que no puede ocurrir sin que un\nhumano aporte la cifra.</p>\n<p>No hecho:</p>\n<ul>\n<li class=\"\"><strong>Sin autenticación en el servidor MCP.</strong> Está enlazado a <code>127.0.0.1</code> y cualquiera en la caja\npuede llamar a todas las herramientas, incluida <code>issue_refund</code>. FastMCP soporta tokens bearer\ny OAuth 2.1; no configuramos ninguno.</li>\n<li class=\"\"><strong>Las herramientas confían por completo en quien llama.</strong> <code>book_slot</code> reservará para cualquier\n<code>customer_id</code> que reciba. No hay noción de <em>quién</em> pregunta — el mismo hueco que la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/\">Parte 4 de la serie de endurecimiento</a>\nencontró entre un plano de control y un plano de datos.</li>\n<li class=\"\"><strong>Un solo servidor.</strong> <code>ClientGroup</code> conecta varios a la vez y prefija los nombres de\nherramienta por servidor; nosotros usamos un único objetivo.</li>\n<li class=\"\"><strong>SQLite, un solo proceso.</strong> Bien para un agente; la guarda a nivel de fila de <code>book_slot</code>\nhace más trabajo del que parece bajo concurrencia real.</li>\n<li class=\"\"><strong><code>langchain.mcp</code> está en beta</strong> y avisa en cada importació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>Governed tools an agent can call</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-sigue\">Qué sigue<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al Qué sigue\" title=\"Enlace directo al Qué sigue\" translate=\"no\">​</a></h2>\n<p><strong>Acotar las herramientas por agente.</strong> Ahora mismo un solo agente tiene las cinco, así que nada\nimpide que el modelo elija <code>issue_refund</code> cuando se le pidió reservar un hueco — la pausa humana\nes la única guarda. El supervisor de la Parte 2 ya reparte entre un agente de agenda y uno de\nfacturación; dale a cada uno solo su porción de <code>adapter.list_tools()</code> y el de agenda no podrá\nemitir un reembolso porque nunca se le dijo que la herramienta existe. Eso es una garantía más\nfuerte que pedírselo amablemente a un modelo.</p>\n<p><strong>Luego autenticación</strong>, para que una llamada a herramienta lleve una identidad en vez de confiar\nen quien alcance el puerto — la misma pregunta que la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/\">Parte 4 de la serie de endurecimiento</a> hizo\nsobre la propia API del gateway, una capa más arriba.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lecturas-adicionales\">Lecturas adicionales<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/#lecturas-adicionales\" class=\"hash-link\" aria-label=\"Enlace directo al Lecturas adicionales\" title=\"Enlace directo al Lecturas adicionales\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.langchain.com/oss/python/langchain/mcp\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">MCP en LangChain</a> — inicio rápido, conexiones, auth, elicitación</li>\n<li class=\"\"><a href=\"https://docs.langchain.com/oss/python/migrate/langchain-mcp-adapters\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Migrar desde <code>langchain-mcp-adapters</code></a></li>\n<li class=\"\"><a href=\"https://gofastmcp.com/clients/client\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Cliente FastMCP</a> — transportes, auth, caché de respuestas</li>\n<li class=\"\"><a href=\"https://modelcontextprotocol.io/specification/2026-07-28\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Especificación MCP <code>2026-07-28</code></a></li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/news/mcp-2026-07-28-spec/\">Nuestro análisis de la revisión de la especificación</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/",
            "title": "Reservar citas y reembolsar facturas desde un agente LangGraph con MCP",
            "summary": "Los agentes de agenda y facturación de las Partes 1 y 2 nunca agendaron ni facturaron nada. Esto les da un calendario, una lista de clientes y un reembolso que ningún modelo puede emitir solo — por MCP, sobre la especificación sin sesiones, con el modelo en WEC Inference. Incluye tres cosas que nadie documenta: tu servidor sigue usando sesiones por defecto, ctx.elicit es la API vieja y su error de era va al modelo, no a ti, y cache=True no hace nada si el servidor no anuncia un TTL.",
            "date_modified": "2026-09-03T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "agentes",
                "langgraph",
                "langchain",
                "mcp",
                "herramientas",
                "autoalojamiento",
                "wec",
                "wec-inference"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__authentik\" src=\"data:image/png;base64,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\" alt=\"authentik\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2255\" height=\"527\" fill=\"none\" viewBox=\"0 0 2255 527\" class=\"tutorialHero__langfuse\"><path fill=\"#1B1917\" d=\"M652.669 116.433c0-10.261-7.683-17.956-17.926-17.956H607V60h87.923v316.366h-42.254zM804.056 379.786c-39.693 0-72.131-27.362-72.131-68.831 0-41.042 28.596-69.686 84.935-69.686h39.693c7.256 0 12.805-5.558 12.805-12.826v-8.55c0-25.651-20.487-38.905-40.547-38.905-18.353 0-33.291 8.123-41.828 26.934h-45.668c11.95-44.462 46.095-65.41 88.349-65.41 40.547 0 82.374 22.231 82.374 77.808v156.046h-42.68v-14.109c0-5.13-5.549-7.695-9.817-4.702-16.646 11.97-32.438 22.231-55.485 22.231m5.975-38.477c18.78 0 34.572-9.406 52.498-27.362 4.695-4.702 6.829-10.688 6.829-17.1v-6.413c0-7.268-5.549-12.826-12.805-12.826H815.58c-27.743 0-40.974 12.398-40.974 31.637 0 17.956 12.377 32.064 35.425 32.064M961.855 376.366V147.642h42.255v19.666c0 5.13 6.4 6.84 10.24 2.992 13.23-12.825 31.59-27.788 60.61-27.788 36.71 0 70.85 23.086 70.85 75.671v158.183h-42.25V227.161c0-29.499-17.93-45.745-39.7-45.745-20.91 0-35 11.971-49.93 29.499-7.26 8.978-9.82 19.238-9.82 30.354v135.097zM1287.48 467c-52.5 0-85.79-24.796-95.61-63.273h46.1c7.25 15.818 20.06 25.651 45.67 25.651 34.14 0 55.48-20.093 55.48-65.838v-7.268c0-5.13-4.27-7.695-9.39-3.42-14.08 12.398-32.01 20.093-49.08 20.093-58.05 0-96.46-44.889-96.46-115.003 0-70.113 44.39-115.43 98.17-115.43 15.79 0 30.3 4.275 44.38 14.963 5.98 4.275 12.38.855 12.38-5.986v-3.847h42.26V363.54c0 72.678-43.97 103.46-93.9 103.46m-2.56-132.959c19.2 0 33.29-8.55 44.81-20.949 7.26-8.122 9.39-13.68 9.39-26.506v-61.563c0-12.826-2.13-20.948-10.67-29.071-9.39-8.978-22.62-14.964-39.69-14.964-35 0-61.89 29.072-61.89 76.954 0 47.883 24.76 76.099 58.05 76.099M1455.92 199.372c0-7.268-5.97-13.253-13.23-13.253h-32.44v-38.477h32.44c7.26 0 13.23-5.985 13.23-13.253v-5.986c0-45.744 23.48-68.403 69.15-68.403h29.02v38.477h-29.45c-17.5 0-26.46 9.833-26.46 29.926v5.986c0 7.268 5.97 13.253 13.23 13.253h42.68v38.477h-42.68c-7.26 0-13.23 5.985-13.23 13.253v176.994h-42.26zM1652.02 381.496c-35.85 0-69.14-23.086-69.14-75.671V147.642h42.25v150.06c0 29.499 17.07 44.889 37.13 44.889 21.77 0 35.85-11.97 50.79-29.499 7.26-8.977 9.82-19.238 9.82-30.354V147.642h42.25v228.724h-42.25V356.7c0-5.131-6.4-6.841-10.24-2.993-13.24 12.826-31.59 27.789-60.61 27.789M1893.57 381.496c-38.84 0-79.39-19.239-90.06-65.838h43.54c6.4 17.528 23.9 29.498 44.81 29.498 23.05 0 37.13-13.68 37.13-30.353 0-16.246-11.09-25.224-28.59-30.354l-36.28-10.261c-31.58-8.978-55.06-29.499-55.06-64.556 0-38.049 35.43-67.12 75.55-67.12 32.01 0 70.85 14.535 81.09 65.41h-40.55c-5.55-17.528-20.06-29.071-40.54-29.071-20.06 0-34.58 12.398-34.58 28.216 0 13.253 8.11 23.086 27.32 28.644l34.15 9.833c32.43 9.406 58.47 29.072 58.47 66.693 0 39.332-34.15 69.259-76.4 69.259M2098.54 381.496c-61.46 0-102.01-51.73-102.01-119.706s43.11-119.278 101.58-119.278c63.6 0 96.89 51.302 96.89 109.872v23.087h-144.26c-5.98 0-8.54 3.847-7.26 13.68 4.7 32.064 30.31 54.295 55.49 54.295 18.78 0 35-9.405 45.67-27.788h44.81c-16.22 40.187-49.94 65.838-90.91 65.838m43.11-141.51c6.83 0 9.39-3.42 7.68-14.108-4.69-26.506-24.33-45.317-51.22-45.317-25.6 0-47.37 18.811-54.2 45.745-2.56 9.833.85 13.68 6.83 13.68z\"></path><path fill=\"#FF5D5F\" d=\"m286.292 286.105 34.597 27.791s26.473-19.661 45.941-22.545c20.418-3.025 42.202 8.359 62.388 21.93 30.489 20.498 56.149 46.508 56.149 46.508l30.06-29.493s-82.879-89.795-148.597-81.672c-43.105 5.328-80.538 37.481-80.538 37.481\"></path><path fill=\"#4E9CFF\" d=\"M88.358 114.862 60 146.056s79.009 73.732 141.224 73.732c28.358 0 67.684-22.216 101.523-51.079 19.283-16.448 40.835-35.13 62.388-35.13 14.487 0 33.594 7.673 51.612 27.824 0 0 11.63-6.974 18.716-11.985 6.228-4.404 15.479-11.91 15.479-11.91-25.918-27.663-63.407-47.883-85.807-45.9-36.299.005-62.388 22.601-94.717 48.735s-45.94 36.907-69.194 36.907c-39.134 0-112.866-62.388-112.866-62.388M88.358 352.463 60 321.269s79.009-73.732 141.224-73.732c28.358 0 67.684 22.216 101.523 51.079 19.283 16.448 40.835 35.13 62.388 35.13 14.556 0 33.518-7.989 51.612-28.358 0 0 10.877 6.705 17.582 11.344 6.894 4.769 17.015 12.655 17.015 12.655-25.931 27.883-63.693 48.323-86.209 46.33-36.299-.005-57.851-19.24-90.179-45.374-32.329-26.133-50.478-40.268-73.732-40.268-39.134 0-112.866 62.388-112.866 62.388M458.142 185.149c-7.378 5.1-19.283 12.478-19.283 12.478s6.806 14.746 6.806 34.597-6.239 36.866-6.239 36.866 10.688 6.675 17.582 11.343c7.162 4.849 18.149 13.045 18.149 13.045s13.045-27.224 13.045-61.254-13.045-59.552-13.045-59.552-10.236 7.792-17.015 12.477\"></path><path fill=\"#FF5D5F\" d=\"m287.995 180.612 32.895-27.224s26.473 19.046 45.941 21.93c20.417 3.026 42.202-8.359 62.388-21.93 30.489-20.498 56.149-46.507 56.149-46.507l30.06 29.492s-82.879 89.795-148.597 81.672c-43.105-5.328-78.836-37.433-78.836-37.433M208.601 91c42.538 0 78.264 36.299 78.264 36.299s-9.941 7.832-16.448 13.045c-6.777 5.429-17.582 14.179-17.582 14.179s-18.711-19.851-44.234-19.851c-10.465 0-24.066 6.286-38.567 18.716-11.188 9.591-22.829 21.514-30.627 36.299-6.743 12.784-10.42 27.85-10.776 43.672-.446 19.873 6.597 40.704 18.149 57.283 7.743 11.112 16.983 19.474 26.657 26.657 12.555 9.322 25.648 15.881 35.164 15.881 10.166 0 19.306-3.533 26.09-6.806 10.776-6.239 19.278-13.612 19.278-13.612l33.463 27.791s-13.612 13.612-32.323 23.821c-12.091 5.963-27.632 11.91-46.508 11.91-18.862 0-40.767-10.022-61.254-25.522-13.244-10.021-26.225-21.895-36.298-36.299-16.51-23.607-25.017-52.328-24.96-81.104.057-29.136 9.451-57.993 26.094-81.672C138.273 117.657 176.86 91 208.601 91\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Lock down Docker networks</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run containers as non-root</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">3</span><span class=\"skillTracker__skill\" data-state=\"current\">Identity in front of every port</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Membership, not just an account</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">An identity for the agent, not a key</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">A tool server that checks who is asking</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Where the agent can go, not just what it can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/\"><span class=\"skillTracker__dot\" data-state=\"locked\">8</span><span class=\"skillTracker__skill\" data-state=\"locked\">Move the daemon off root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/\"><span class=\"skillTracker__dot\" data-state=\"locked\">9</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run the model's own code without trusting it</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">A scope per tool, and a refusal clients can act on</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>La <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/\">Parte 1</a> cerró los puertos que nadie\nquiso abrir. La <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/\">Parte 2</a> impidió que dos\ncontenedores se ejecutaran como root. Ambas trataban sobre la máquina. Ninguna tocó la\npregunta que una pila de IA autoalojada responde peor: <strong>¿quién tiene permiso para iniciar\nsesión, y dónde se decide eso?</strong></p>\n<p>La misma caja de antes. El Langfuse que venimos endureciendo desde la Parte 1 — aquel cuyo\nPostgres, ClickHouse y Redis la Parte 1 encontró correctamente enlazados a localhost, y\ncuyo worker la Parte 2 bajó de root — se desplegó en\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/\">Detecta lo que tus pruebas no ven</a>. Dos\npartes han asegurado ya la máquina por debajo sin tocar ni una vez la puerta de entrada de\nla propia aplicación. Esta es la primera parte que cambia Langfuse en sí.</p>\n<p>Ahora mismo esa decisión se toma en cada app, por separado. Langfuse tiene su propia tabla\nde correo y contraseña. También la tiene cualquier otra herramienta en la caja. Cada una es\nun lugar donde una cuenta puede sobrevivir a la persona que la tenía, donde una contraseña\npuede reutilizarse, y donde \"quitarle el acceso a esta persona\" significa acordarse de que\nesa app existe.</p>\n<p>Este post mueve esa decisión a un solo lugar. Ese lugar es\n<a href=\"https://goauthentik.io/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">authentik</a> — un proveedor de identidad de código abierto que\nejecutas tú mismo, la contraparte autoalojada de Okta o Auth0. Guarda las cuentas, muestra\nla pantalla de inicio de sesión, y da fe de quién es alguien ante cualquier app que\npregunte. Las apps dejan de almacenar contraseñas y empiezan a preguntarle a authentik.</p>\n<p>Es el mismo trabajo que hace Keycloak, y Keycloak es el nombre más conocido. authentik se\ngana la elección aquí por el coste de puesta en marcha: un archivo compose y un asistente\nfrente a los realms, clients y ajustes de JVM de Keycloak. Para una caja y una app, esa\ndiferencia es toda la decisión.</p>\n<p>Lo desplegamos, conectamos Langfuse a él por OIDC, y terminamos con un inicio de sesión que\npasa por authentik y vuelve.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>Una WEC Instance: 8 vCPU AMD EPYC 7601, 15 GB RAM, Docker 29.1.3, kernel 5.15.\nauthentik <code>2026.8.1</code> desde <code>ghcr.io/goauthentik/server</code>, Postgres 16. Langfuse v3\n(<code>langfuse/langfuse:3</code>) — el despliegue heredado de las Partes 1 y 2, accesible en\n<code>10.80.4.212:3001</code>. authentik publicado en el puerto host <code>9100</code>.</p><p>Las direcciones de este post son una LAN privada. Sustituye las tuyas. Todo aquí es HTTP\nplano sobre una red de confianza, lo cual está bien para una primera ejecución y <strong>no</strong> lo\nestá para nada accesible desde fuera — mira <a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#qu%C3%A9-sigue\" class=\"\">Qué sigue</a>.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-vocabulario-antes-de-los-clics\">El vocabulario, antes de los clics<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#el-vocabulario-antes-de-los-clics\" class=\"hash-link\" aria-label=\"Enlace directo al El vocabulario, antes de los clics\" title=\"Enlace directo al El vocabulario, antes de los clics\" translate=\"no\">​</a></h2>\n<p>Toda guía de OIDC asume que ya conoces estas cinco palabras. Tenerlas claras de entrada es\nla diferencia entre configurar esto una vez y adivinar campos durante una hora.</p>\n<p><strong>OP y RP.</strong> El <strong>OpenID Provider</strong> es lo que sabe quién es la gente — authentik. La\n<strong>Relying Party</strong> es la app que quiere que se lo digan — Langfuse. La RP nunca ve una\ncontraseña. Recibe una declaración firmada del OP que dice \"este es <code>admin@example.com</code>, yo\nlo verifiqué\".</p>\n<p><strong>Proveedor vs aplicación.</strong> authentik separa lo que la mayoría de herramientas fusiona. Un\n<strong>proveedor</strong> es el endpoint del protocolo — la maquinaria OIDC, el client ID y el secreto,\nla URI de redirección. Una <strong>aplicación</strong> es lo que los usuarios ven y aquello a lo que se\nenganchan las reglas de acceso. Se emparejan uno a uno: una aplicación, un proveedor, y el\nasistente del Paso 3 crea ambos juntos.</p>\n<p><strong>La URI de redirección</strong> es a dónde authentik tiene permiso de enviar al usuario tras un\ninicio de sesión exitoso. Se compara de forma exacta y es la causa de fallo más común. Ni un\nprefijo, ni un nombre de host — la URL completa, carácter por carácter.</p>\n<p><strong>Scopes vs grant types.</strong> Un <strong>scope</strong> es qué información pide la app\n(<code>openid email profile</code>). Un <strong>grant type</strong> es el mecanismo con el que la pide\n(<code>authorization_code</code>). Preguntas distintas; la interfaz las pone cerca y es fácil\nconfundirlas.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-hace-realmente-el-inicio-de-sesión\">Qué hace realmente el inicio de sesión<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#qu%C3%A9-hace-realmente-el-inicio-de-sesi%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al Qué hace realmente el inicio de sesión\" title=\"Enlace directo al Qué hace realmente el inicio de sesión\" translate=\"no\">​</a></h2>\n<!-- -->\n<p>Lo que vale la pena notar: el código viaja por el navegador, el secreto nunca. Ese\nintercambio del penúltimo paso ocurre entre los dos contenedores directamente, que es por lo\nque el secreto del cliente es un ajuste de lado servidor y por lo que Langfuse debe poder\nalcanzar a authentik por la red — no solo tu navegador.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">Un Langfuse en ejecución, idealmente el de la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/\">Parte 1</a></li>\n<li class=\"\">Docker con el plugin Compose</li>\n<li class=\"\">Un puerto host libre para authentik (usamos <code>9100</code>)</li>\n<li class=\"\">La IP LAN de la caja, no <code>localhost</code> — dos contenedores necesitan alcanzarse entre sí y tu\nnavegador necesita alcanzar a ambos</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--desplegar-authentik\">Paso 1 — Desplegar authentik<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#paso-1--desplegar-authentik\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Desplegar authentik\" title=\"Enlace directo al Paso 1 — Desplegar authentik\" translate=\"no\">​</a></h2>\n<p>authentik trae un archivo compose y genera sus propios secretos. Crea un directorio y\ndescarga ambos:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">mkdir</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-O</span><span class=\"token plain\"> https://goauthentik.io/docker-compose.yml</span><br></div></code></pre></div></div>\n<p>Ahora el archivo de entorno. authentik necesita una contraseña de Postgres y una clave\nsecreta, ambas aleatorias, más los puertos que publicará:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"PG_PASS=</span><span class=\"token string variable\" style=\"color:#36acaa\">$(</span><span class=\"token string variable\" style=\"color:#36acaa\">openssl rand </span><span class=\"token string variable parameter variable\" style=\"color:#36acaa\">-base64</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable number\" style=\"color:#36acaa\">36</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable operator\" style=\"color:#393A34\">|</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable function\" style=\"color:#d73a49\">tr</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable string\" style=\"color:#e3116c\">'\\n'</span><span class=\"token string variable\" style=\"color:#36acaa\">)</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"AUTHENTIK_SECRET_KEY=</span><span class=\"token string variable\" style=\"color:#36acaa\">$(</span><span class=\"token string variable\" style=\"color:#36acaa\">openssl rand </span><span class=\"token string variable parameter variable\" style=\"color:#36acaa\">-base64</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable number\" style=\"color:#36acaa\">60</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable operator\" style=\"color:#393A34\">|</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable function\" style=\"color:#d73a49\">tr</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable string\" style=\"color:#e3116c\">'\\n'</span><span class=\"token string variable\" style=\"color:#36acaa\">)</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"COMPOSE_PORT_HTTP=9100\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"COMPOSE_PORT_HTTPS=9543\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> .env</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">chmod</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">600</span><span class=\"token plain\"> .env</span><br></div></code></pre></div></div>\n<p><code>AUTHENTIK_SECRET_KEY</code> firma sesiones y tokens. Si la pierdes, toda sesión existente y todo\ntoken emitido quedan inválidos — así que pertenece a tu gestor de contraseñas, no solo a\neste archivo.</p>\n<p>Levántalo. El primer arranque ejecuta migraciones de base de datos y tarda un minuto o dos:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><br></div></code></pre></div></div>\n<p>Tres contenedores — <code>server</code>, <code>worker</code> y <code>postgresql</code> — todos reportando <code>(healthy)</code>. Los\narchivos compose más antiguos de authentik también traían un servicio <code>redis</code> aparte;\n<code>2026.8.1</code> ya no lo hace, así que tres es correcto y no falta nada. <code>server</code> es el que tiene\nque estar saludable antes de que continúes.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker compose ps mostrando los contenedores server, worker y postgresql de authentik todos reportando healthy\" src=\"https://development-wec.wiline.com/docs/es/assets/images/authentik-compose-ps-2cdcd74dfeb2be5f3c8d9bb137b646db.png\" width=\"1127\" height=\"102\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--reclamar-la-cuenta-de-administrador\">Paso 2 — Reclamar la cuenta de administrador<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#paso-2--reclamar-la-cuenta-de-administrador\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — Reclamar la cuenta de administrador\" title=\"Enlace directo al Paso 2 — Reclamar la cuenta de administrador\" translate=\"no\">​</a></h2>\n<p>El primer arranque de authentik deja la cuenta de administrador sin reclamar. La URL de\nconfiguración existe exactamente una vez; visitarla es cómo tomas posesión.</p>\n<p>Abre <code>http://10.80.4.212:9100/if/flow/initial-setup/</code>, y establece el correo y la contraseña\ndel administrador por defecto (<code>akadmin</code>).</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La pantalla de configuración inicial de authentik, estableciendo el correo y la contraseña de la cuenta akadmin por defecto\" src=\"https://development-wec.wiline.com/docs/es/assets/images/authentik-initial-setup-e170a5fefdbc3d6a5b645819857b6cae.png\" width=\"1900\" height=\"955\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Usa una dirección que controles. Esta cuenta puede hacer de todo, así que dale una\ncontraseña de verdad y guárdala en algún lugar duradero.</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>Esta URL es una llave de un solo uso</div><div class=\"admonitionContent_BuS1\"><p>Hasta que alguien complete este formulario, cualquiera que pueda alcanzar el puerto puede\nconvertirse en administrador de tu proveedor de identidad. Hazlo inmediatamente después de\nque los contenedores levanten — no mañana.</p></div></div>\n<p>Aterrizas en el Application Dashboard — la vista de cara al usuario, no la de administración,\ny vacía porque no hay nada configurado todavía.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El Application Dashboard de authentik indicando No Applications available, con un botón Create a new application\" src=\"https://development-wec.wiline.com/docs/es/assets/images/authentik-app-dashboard-empty-fb4aaff0a94a00ac91abe85b9541079d.png\" width=\"1902\" height=\"954\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><code>Create a new application</code> aquí lleva al mismo sitio que la interfaz de administración; el\nbotón <strong>Admin interface</strong> arriba a la derecha es cómo llegas a todo lo demás.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--ejecutar-el-asistente-de-aplicación\">Paso 3 — Ejecutar el asistente de aplicación<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#paso-3--ejecutar-el-asistente-de-aplicaci%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — Ejecutar el asistente de aplicación\" title=\"Enlace directo al Paso 3 — Ejecutar el asistente de aplicación\" translate=\"no\">​</a></h2>\n<p><strong>Applications → Applications → Create</strong>. authentik te lleva por cinco pasos, y vale la pena\nleer la lista de pasos de la izquierda antes de tocar nada:</p>\n<p><strong>Application → Choose a Provider → Configure Provider → Configure Bindings → Review and\nSubmit</strong></p>\n<p>Ese es el vocabulario de antes, en orden. El asistente crea la aplicación y su proveedor en\nuna sola pasada y los empareja por ti. (<code>Applications → Providers → Create</code> construye un\nproveedor por su cuenta, si alguna vez necesitas uno antes que la app que lo usa.)</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"31--application\">3.1 — Application<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#31--application\" class=\"hash-link\" aria-label=\"Enlace directo al 3.1 — Application\" title=\"Enlace directo al 3.1 — Application\" translate=\"no\">​</a></h3>\n<ul>\n<li class=\"\"><strong>Application Name</strong> — <code>Langfuse</code>. La etiqueta que los usuarios ven en el dashboard.</li>\n<li class=\"\"><strong>Slug</strong> — <code>langfuse</code>. Carga peso: pasa a formar parte de la URL del emisor, así que\ncambiarlo después cambia la URL con la que tienes que reconfigurar Langfuse.</li>\n<li class=\"\"><strong>Group</strong> — déjalo vacío. Esto agrupa apps en el dashboard; no tiene nada que ver con\ngrupos de usuarios ni control de acceso, pese al nombre.</li>\n<li class=\"\"><strong>Policy engine mode</strong> — déjalo en <strong>ANY</strong>. Decide cómo se combinan múltiples políticas\nvinculadas. Sin políticas vinculadas, todavía no supone diferencia.</li>\n</ul>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El paso Application del asistente, mostrando los campos Name, Slug, Group y Policy engine mode antes de introducir nada\" src=\"https://development-wec.wiline.com/docs/es/assets/images/authentik-wizard-application-62b3555c7b9b05dedfdd535153d2c084.png\" width=\"1901\" height=\"960\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"32--choose-a-provider\">3.2 — Choose a Provider<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#32--choose-a-provider\" class=\"hash-link\" aria-label=\"Enlace directo al 3.2 — Choose a Provider\" title=\"Enlace directo al 3.2 — Choose a Provider\" translate=\"no\">​</a></h3>\n<p>Ocho tipos de proveedor. Toma <strong>OAuth2/OpenID Provider</strong> — \"OAuth2 Provider for generic\nOAuth and OpenID Connect Applications.\"</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El paso de tipo de proveedor del asistente con ocho tarjetas y OAuth2/OpenID Provider seleccionado\" src=\"https://development-wec.wiline.com/docs/es/assets/images/authentik-wizard-provider-type-62af4193167bfaa5359808b786af8d41.png\" width=\"1898\" height=\"958\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Vale la pena saber qué <em>no</em> estás eligiendo, porque dos de estos resuelven un problema\ndistinto: <strong>Proxy Provider</strong> pone a authentik delante de una app que no soporta SSO por sí\nmisma, y <strong>LDAP Provider</strong> expone authentik a cosas que solo hablan LDAP. Langfuse habla\nOIDC de forma nativa, así que recibe un proveedor OIDC de verdad.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"33--configure-provider\">3.3 — Configure Provider<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#33--configure-provider\" class=\"hash-link\" aria-label=\"Enlace directo al 3.3 — Configure Provider\" title=\"Enlace directo al 3.3 — Configure Provider\" translate=\"no\">​</a></h3>\n<p>El paso que importa, y el que te costará tiempo si lo apuras.</p>\n<ul>\n<li class=\"\">\n<p><strong>Authorization flow</strong> — <code>default-provider-authorization-explicit-consent</code>. \"Explicit\"\nmuestra al usuario una pantalla de consentimiento que lista lo que Langfuse pidió. La\nvariante implícita se la salta. Elige explícito para una primera construcción: esa pantalla\nes una lectura en vivo de tu configuración de scopes, lo que hace visible una mala\nconfiguración en lugar de silenciosa.</p>\n</li>\n<li class=\"\">\n<p><strong>Client type</strong> — <strong>Confidential</strong> (desplazado por encima de los campos de redirección).\nLangfuse es un servidor y puede guardar un secreto, así que recibe uno. Los clientes\npúblicos son para navegadores y apps móviles que no pueden.</p>\n</li>\n<li class=\"\">\n<p><strong>Redirect URIs/Origins (RegEx)</strong> — el campo con dos desplegables y un valor. Pon el modo\nen <strong>Strict</strong>, el propósito en <strong>Authorization</strong>, y la URI en el callback de NextAuth de\nLangfuse:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">http://10.80.4.212:3001/api/auth/callback/custom</span><br></div></code></pre></div></div>\n<p>El <code>custom</code> final no es un marcador de posición — es el ID de proveedor que NextAuth\nasigna a su proveedor OIDC genérico. Usa tu propio host y puerto, pero esa ruta es fija.</p>\n</li>\n<li class=\"\">\n<p><strong>Signing Key</strong> — deja <code>authentik Self-signed Certificate</code>. Esto firma el token de ID;\nLangfuse obtiene la clave pública correspondiente del <code>jwks_uri</code> en el Paso 5 y verifica\ncontra ella. Autofirmado es lo correcto aquí — la RP confía en esta clave porque el\ndescubrimiento se la entregó, no porque una CA responda por ella.</p>\n</li>\n<li class=\"\">\n<p><strong>Logout URI</strong> y las validez de los tokens — déjalos en paz.</p>\n</li>\n</ul>\n<p>El proveedor una vez guardado — flujo de autorización con consentimiento explícito, tipo de\ncliente Confidential, y Authorization Code como grant:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El formulario de edición del proveedor OAuth2/OpenID mostrando el flujo de autorización de consentimiento explícito, Client Type en Confidential, y Authorization Code marcado\" src=\"https://development-wec.wiline.com/docs/es/assets/images/authentik-wizard-configure-provider-11b9bd02c65bf52f23d2afe34f3a8be9.png\" width=\"1896\" height=\"948\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Strict significa strict</div><div class=\"admonitionContent_BuS1\"><p>El desplegable de modo también ofrece <strong>Regex</strong>, y el propio texto de ayuda de authentik\nseñala que puedes ponerlo en <code>.*</code> para permitir cualquier URI de redirección. No lo hagas.\nUna URI de redirección abierta significa que cualquiera que pueda iniciar un login puede\nhacer que el código de autorización se entregue a un host que él controla. Strict, con una\nURL exacta, es el sentido entero del campo.</p></div></div>\n<p>Más abajo, <strong>Scopes</strong> viene precargado con cuatro mapeos: <code>email</code>, <code>openid</code>, <code>profile</code> y\n<code>offline_access</code>. Déjalos. <code>openid</code> es obligatorio y es lo que marca esto como OIDC en lugar\nde OAuth2 a secas. <code>email</code> es el claim con el que Langfuse identifica la cuenta. <code>profile</code>\nlleva el nombre visible. <code>offline_access</code> es el que el asistente añade sin preguntar —\npermite un token de refresco, de modo que una sesión puede renovarse sin mandar al usuario de\nvuelta por el flujo de login.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"34--configure-bindings\">3.4 — Configure Bindings<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#34--configure-bindings\" class=\"hash-link\" aria-label=\"Enlace directo al 3.4 — Configure Bindings\" title=\"Enlace directo al 3.4 — Configure Bindings\" translate=\"no\">​</a></h3>\n<p><code>No bound policies.</code> — y lo vamos a dejar así.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El paso Configure Bindings del asistente indicando No bound policies\" src=\"https://development-wec.wiline.com/docs/es/assets/images/authentik-wizard-bindings-b5cb6f9b81084f2eb498d0b4cdeae6cd.png\" width=\"1896\" height=\"954\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>Este es el hueco, y es deliberado</div><div class=\"admonitionContent_BuS1\"><p>Lee la descripción del propio asistente: <em>\"These policies control which users can access this\napplication.\"</em> Sin nada vinculado, la respuesta es <strong>todo el que pueda autenticarse</strong>. En una\ncaja donde eres la única cuenta, eso es lo mismo que \"solo yo\" — que es por lo que resulta\ntolerable aquí y por lo que el asistente te deja saltártelo.</p><p>No es control de acceso. Toda cuenta que crees en este authentik entra en Langfuse por\ndefecto, y nada te avisará. Los grupos y las vinculaciones de políticas son cómo se arregla\neso, y son el tema del siguiente post — donde lo que se protege es un gateway y la\nrespuesta importa muchísimo más.</p></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"35--review-and-submit\">3.5 — Review and Submit<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#35--review-and-submit\" class=\"hash-link\" aria-label=\"Enlace directo al 3.5 — Review and Submit\" title=\"Enlace directo al 3.5 — Review and Submit\" translate=\"no\">​</a></h3>\n<p>Revisa el slug y la URI de redirección una vez más, y envía.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--leer-la-configuración-de-vuelta-desde-authentik\">Paso 4 — Leer la configuración de vuelta desde authentik<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#paso-4--leer-la-configuraci%C3%B3n-de-vuelta-desde-authentik\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — Leer la configuración de vuelta desde authentik\" title=\"Enlace directo al Paso 4 — Leer la configuración de vuelta desde authentik\" translate=\"no\">​</a></h2>\n<p>En lugar de ensamblar URLs a mano, pregúntale a authentik. Todo proveedor OIDC publica un\ndocumento de descubrimiento, y el de authentik vive bajo el slug de la aplicación:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://10.80.4.212:9100/application/o/langfuse/.well-known/openid-configuration </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token string\" style=\"color:#e3116c\">'{issuer, authorization_endpoint, token_endpoint, userinfo_endpoint, jwks_uri, grant_types_supported}'</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"issuer\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://10.80.4.212:9100/application/o/langfuse/\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"authorization_endpoint\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://10.80.4.212:9100/application/o/authorize/\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"token_endpoint\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://10.80.4.212:9100/application/o/token/\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"userinfo_endpoint\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://10.80.4.212:9100/application/o/userinfo/\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"jwks_uri\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://10.80.4.212:9100/application/o/langfuse/jwks/\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"grant_types_supported\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"authorization_code\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"refresh_token\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"implicit\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"client_credentials\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"password\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"urn:ietf:params:oauth:grant-type:device_code\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>Dos cosas que sacar de esto. El <strong>issuer</strong> es la única URL que Langfuse necesita — él mismo\nobtendrá este documento y descubrirá el resto. Y <code>grant_types_supported</code> lista lo que el\nprotocolo ofrece, no lo que tu proveedor aceptará; nosotros usamos <code>authorization_code</code> y la\npresencia de <code>password</code> en esa lista no es una invitación.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El documento de descubrimiento devuelto por el authentik en ejecución, mostrando el issuer, los endpoints de authorize, token y userinfo, la URL JWKS y los grant types soportados\" src=\"https://development-wec.wiline.com/docs/es/assets/images/authentik-discovery-json-959875837069e04a0b42b7cc81ad0b75.png\" width=\"860\" height=\"313\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Ahora las credenciales del cliente. Están en la página de detalle del proveedor en la\ninterfaz, pero <code>docker compose exec</code> es más rápido y se copia y pega limpio:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.core.models import Application</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">p = Application.objects.get(slug='langfuse').get_provider()</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('PROVIDER     :', p.name)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('CLIENT_ID    :', p.client_id)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('CLIENT_SECRET:', p.client_secret)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div></code></pre></div></div>\n<p>Fíjate en que esto busca el proveedor <em>a través del slug de la aplicación</em> y no por el nombre\ndel proveedor. El asistente nombró a tu proveedor <code>Provider for Langfuse</code>, no <code>langfuse</code> — el\nslug es lo que tú elegiste y lo que se mantiene predecible. <code>get_provider()</code> también importa:\nrecurrir a <code>application.provider</code> devuelve la clase base con <code>client_id</code> vacío, lo que se\nparece alarmantemente a un proveedor roto y no lo es.</p>\n<p>Mantén esa salida fuera del historial de tu shell y fuera de las capturas de pantalla.</p>\n<p>La página de detalle del proveedor lleva los mismos valores, por si prefieres leerlos ahí —\ntipo de cliente, client ID, la URI de redirección estricta, y cada URL de descubrimiento en\nun solo sitio. El secreto del cliente es lo único que no imprime:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La página de resumen de Provider for Langfuse listando tipo de cliente, client ID, la URI de redirección estricta y las URLs de configuración OpenID, authorize, token, userinfo y JWKS\" src=\"https://development-wec.wiline.com/docs/es/assets/images/authentik-provider-overview-5b451791d184236f85921a5a394f956e.png\" width=\"1897\" height=\"952\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5--apuntar-langfuse-hacia-él\">Paso 5 — Apuntar Langfuse hacia él<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#paso-5--apuntar-langfuse-hacia-%C3%A9l\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 5 — Apuntar Langfuse hacia él\" title=\"Enlace directo al Paso 5 — Apuntar Langfuse hacia él\" translate=\"no\">​</a></h2>\n<p>Langfuse lee ajustes OIDC genéricos desde variables <code>AUTH_CUSTOM_*</code>. Añádelas a\n<code>~/langfuse/.env</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">AUTH_CUSTOM_NAME</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">authentik</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">AUTH_CUSTOM_ISSUER</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">http://10.80.4.212:9100/application/o/langfuse</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">AUTH_CUSTOM_CLIENT_ID</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">el client </span><span class=\"token function\" style=\"color:#d73a49\">id</span><span class=\"token plain\"> del Paso </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">4</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">AUTH_CUSTOM_CLIENT_SECRET</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">el client secret del Paso </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">4</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">AUTH_CUSTOM_SCOPE</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">openid email profile</span><br></div></code></pre></div></div>\n<p>El issuer aquí no lleva barra final mientras que el documento de descubrimiento reportó una.\nAmbos funcionan — Langfuse lo normaliza.</p>\n<p>Confirma también que <code>NEXTAUTH_URL</code> coincide con la dirección que realmente navegas:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> NEXTAUTH_URL ~/langfuse/.env</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">NEXTAUTH_URL=http://10.80.4.212:3001</span><br></div></code></pre></div></div>\n<p>Si esto dice <code>localhost</code> mientras tu URI de redirección dice <code>10.80.4.212</code>, el callback se\nconstruirá con el nombre de host equivocado y authentik lo rechazará.</p>\n<p><strong>Ahora la parte que te hará perder la tarde si te la saltas.</strong> Escribir esas variables en\n<code>.env</code> no basta. El archivo compose que trae Langfuse lista explícitamente las variables que\npasa al contenedor <code>langfuse-web</code>, y <code>AUTH_CUSTOM_*</code> no está entre ellas — aterrizan en\n<code>.env</code>, Compose las lee para interpolación, y nunca llegan al proceso. Langfuse arranca bien\ny no muestra ningún botón de SSO, sin nada en los registros que diga por qué.</p>\n<p>Pásalas con un archivo de override, <code>~/langfuse/docker-compose.override.yml</code>:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">services</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">langfuse-web</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">environment</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">AUTH_CUSTOM_NAME</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> $</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">AUTH_CUSTOM_NAME</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">AUTH_CUSTOM_ISSUER</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> $</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">AUTH_CUSTOM_ISSUER</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">AUTH_CUSTOM_CLIENT_ID</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> $</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">AUTH_CUSTOM_CLIENT_ID</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">AUTH_CUSTOM_CLIENT_SECRET</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> $</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">AUTH_CUSTOM_CLIENT_SECRET</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">AUTH_CUSTOM_SCOPE</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> $</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">AUTH_CUSTOM_SCOPE</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">AUTH_CUSTOM_ALLOW_ACCOUNT_LINKING</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"true\"</span><br></div></code></pre></div></div>\n<p>Compose fusiona <code>docker-compose.override.yml</code> automáticamente, así que el archivo original\nqueda intacto y sobrevive a las actualizaciones. Esa última variable es el arreglo de un\nfallo que si no te encontrarías en el siguiente paso — la sección de resolución de problemas\nexplica qué hace y qué cuesta.</p>\n<p>Recrea el contenedor web y verifica que las variables llegaron:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/langfuse </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> langfuse-web</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/langfuse </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> langfuse-web </span><span class=\"token function\" style=\"color:#d73a49\">env</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> AUTH_CUSTOM</span><br></div></code></pre></div></div>\n<p>Seis. Si es cero, el override no se está recogiendo — comprueba que estás ejecutando compose\ndesde <code>~/langfuse</code> y que el nombre del archivo es exactamente <code>docker-compose.override.yml</code>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker compose exec contando seis variables AUTH_CUSTOM dentro del contenedor langfuse-web\" src=\"https://development-wec.wiline.com/docs/es/assets/images/langfuse-env-count-c57cd6125a9c2dbc129ef850beee6378.png\" width=\"477\" height=\"82\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-6--iniciar-sesión\">Paso 6 — Iniciar sesión<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#paso-6--iniciar-sesi%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 6 — Iniciar sesión\" title=\"Enlace directo al Paso 6 — Iniciar sesión\" translate=\"no\">​</a></h2>\n<p>Abre <code>http://10.80.4.212:3001/auth/sign-in</code>, en una ventana privada para no estar mirando una\nsesión existente. Hay un botón nuevo bajo el separador debajo del formulario de contraseña.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La página de inicio de sesión de Langfuse con un botón de authentik debajo del formulario de correo y contraseña\" src=\"https://development-wec.wiline.com/docs/es/assets/images/langfuse-sso-button-40985ca287c0686b8a2c65f8b7998d3d.png\" width=\"2838\" height=\"1634\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>El botón se llama <code>authentik</code> porque ese es tu valor de <code>AUTH_CUSTOM_NAME</code>, renderizado tal\ncual. Ponlo en <code>Company SSO</code> y eso es lo que dirá el botón — es una cadena de visualización y\nnada más depende de ella.</p>\n<p>Otras dos cosas en esa captura vale la pena nombrarlas en lugar de pasarlas por alto. El\nformulario de correo y contraseña sigue ahí, y también <strong>Sign up</strong> — el SSO se ha <em>añadido</em>,\nno sustituido. Y la barra de direcciones dice <strong>Not Secure</strong>, correctamente: esto es HTTP\nplano.</p>\n<p>Haz clic en el botón y te entregan a authentik, que te dice de dónde vienes:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La pantalla de inicio de sesión de authentik indicando Log in to continue to Langfuse\" src=\"https://development-wec.wiline.com/docs/es/assets/images/authentik-login-langfuse-f20d6f5f65651cb88e1db77bdd2f7c07.png\" width=\"1900\" height=\"987\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Inicia sesión, y como elegimos el flujo de consentimiento explícito en el Paso 3.3, authentik\nmuestra lo que Langfuse pidió antes de entregar nada:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La pantalla de consentimiento de authentik, Redirecting to Langfuse, listando Email address y General Profile Information\" src=\"https://development-wec.wiline.com/docs/es/assets/images/authentik-consent-1db931f804b8b9ddaa8562e01b588adf.png\" width=\"1900\" height=\"990\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Dice <code>You're about to sign into Langfuse</code>, te identifica como <code>akadmin</code>, y lista dos permisos\n— <strong>Email address</strong> y <strong>General Profile Information</strong>. Esos son los scopes <code>email</code> y <code>profile</code>\nen forma legible. <code>openid</code> no recibe una línea propia: no lleva datos personales que consentir,\nes la bandera que hace que esto sea OIDC en lugar de OAuth2 a secas. Así que una configuración\nde tres scopes produce una pantalla de consentimiento de dos elementos, lo cual es correcto y\nno señal de que algo se haya perdido.</p>\n<p>Continúa, y aterrizas en Langfuse, con la sesión iniciada, como la identidad por la que\nauthentik respondió.</p>\n<p>Ese es el bucle cerrado. Langfuse nunca vio una contraseña.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"resolución-de-problemas--los-errores-que-esta-ejecución-produjo-de-verdad\">Resolución de problemas — los errores que esta ejecución produjo de verdad<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#resoluci%C3%B3n-de-problemas--los-errores-que-esta-ejecuci%C3%B3n-produjo-de-verdad\" class=\"hash-link\" aria-label=\"Enlace directo al Resolución de problemas — los errores que esta ejecución produjo de verdad\" title=\"Enlace directo al Resolución de problemas — los errores que esta ejecución produjo de verdad\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"oauthaccountnotlinked\"><code>OAuthAccountNotLinked</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#oauthaccountnotlinked\" class=\"hash-link\" aria-label=\"Enlace directo al oauthaccountnotlinked\" title=\"Enlace directo al oauthaccountnotlinked\" translate=\"no\">​</a></h3>\n<p>El primer intento de inicio de sesión rebotó de vuelta a la página de login de Langfuse con\n<code>?error=OAuthAccountNotLinked</code> en la URL y nada más.</p>\n<p>La causa: ya existía una cuenta de Langfuse de correo y contraseña con la misma dirección que\nauthentik estaba presentando ahora. Lo que NextAuth hace por defecto es rechazar la fusión.\nEse comportamiento por defecto es protector — si un proveedor de identidad puede afirmar\ncualquier correo y la app vincula automáticamente solo por el correo, entonces quien controle\nel proveedor puede apoderarse de una cuenta existente reclamando su dirección.</p>\n<p><code>AUTH_CUSTOM_ALLOW_ACCOUNT_LINKING: \"true\"</code> le dice a Langfuse que las vincule de todos modos.\nEs seguro <strong>aquí</strong> porque tú eres dueño del único proveedor y controlas quién puede crear\ncuentas en él. No sería seguro apuntando a un proveedor donde cualquiera pueda auto-registrar\nun correo arbitrario.</p>\n<p>La alternativa, si prefieres no habilitarlo: borra la cuenta local preexistente y deja que el\nSSO cree una nueva.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"desajuste-de-redirect_uri-o-authentik-negándose-a-redirigir\">Desajuste de <code>redirect_uri</code>, o authentik negándose a redirigir<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#desajuste-de-redirect_uri-o-authentik-neg%C3%A1ndose-a-redirigir\" class=\"hash-link\" aria-label=\"Enlace directo al desajuste-de-redirect_uri-o-authentik-negándose-a-redirigir\" title=\"Enlace directo al desajuste-de-redirect_uri-o-authentik-negándose-a-redirigir\" translate=\"no\">​</a></h3>\n<p>Lee la URL que falla en la barra de direcciones y compárala con la URI de redirección del\nproveedor carácter por carácter. En esta ejecución los desajustes que vale la pena nombrar\nfueron la ruta final <code>/api/auth/callback/custom</code> (cualquier otro sufijo falla), <code>localhost</code>\nfrente a la IP LAN, y una barra final suelta. Comparación estricta significa estricta.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"sin-botón-de-sso-sin-errores\">Sin botón de SSO, sin errores<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#sin-bot%C3%B3n-de-sso-sin-errores\" class=\"hash-link\" aria-label=\"Enlace directo al Sin botón de SSO, sin errores\" title=\"Enlace directo al Sin botón de SSO, sin errores\" translate=\"no\">​</a></h3>\n<p>Esa es la trampa del entorno de compose del Paso 5. Comprueba que las variables llegaron al\ncontenedor:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/langfuse </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> langfuse-web </span><span class=\"token function\" style=\"color:#d73a49\">env</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> AUTH_CUSTOM_ISSUER</span><br></div></code></pre></div></div>\n<p>Salida vacía significa que Langfuse está corriendo sin la configuración que nunca supo que le\nfaltaba.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-descubrimiento-falla-desde-dentro-del-contenedor\">El descubrimiento falla desde dentro del contenedor<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#el-descubrimiento-falla-desde-dentro-del-contenedor\" class=\"hash-link\" aria-label=\"Enlace directo al El descubrimiento falla desde dentro del contenedor\" title=\"Enlace directo al El descubrimiento falla desde dentro del contenedor\" translate=\"no\">​</a></h3>\n<p>Que tu navegador alcance a authentik no es prueba de que Langfuse pueda. El intercambio de\ntokens es de contenedor a contenedor:</p>\n<p>La imagen de <code>langfuse-web</code> no trae <code>curl</code>, así que usa el runtime de Node que ya está ahí\ndentro — <code>fetch</code> viene incorporado:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/langfuse </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> langfuse-web </span><span class=\"token function\" style=\"color:#d73a49\">node</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-e</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token string\" style=\"color:#e3116c\">\"fetch('http://10.80.4.212:9100/application/o/langfuse/.well-known/openid-configuration').then(r=&gt;console.log(r.status)).catch(e=&gt;console.log('FAIL',e.message))\"</span><br></div></code></pre></div></div>\n<p><code>200</code> es lo que quieres. <code>FAIL</code> con un error de conexión significa que el contenedor no puede\nalcanzar authentik en absoluto, que es justo lo que se está probando.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La llamada node fetch ejecutada dentro de langfuse-web imprimiendo 200\" src=\"https://development-wec.wiline.com/docs/es/assets/images/langfuse-discovery-200-f17cc0564e966cf97ba8d8a4cc9e7c9f.png\" width=\"533\" height=\"120\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Esta es también la razón por la que el issuer es una IP LAN en lugar de <code>localhost</code> — dentro\ndel contenedor de Langfuse, <code>localhost</code> es el contenedor de Langfuse.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes ejecutar tu propio proveedor de identidad y poner una app detrás de él por OIDC —\nleyendo el issuer, el client ID y el secreto desde authentik en lugar de ensamblar URLs a\nmano, y reconociendo los dos fallos que de verdad te detienen: un archivo compose que nunca\npasa las variables, y <code>OAuthAccountNotLinked</code>.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-te-compró-esto-y-qué-no\">Qué te compró esto y qué no<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#qu%C3%A9-te-compr%C3%B3-esto-y-qu%C3%A9-no\" class=\"hash-link\" aria-label=\"Enlace directo al Qué te compró esto y qué no\" title=\"Enlace directo al Qué te compró esto y qué no\" translate=\"no\">​</a></h2>\n<p>Hecho: un proveedor de identidad, un lugar donde viven las cuentas, un lugar para revocarlas.\nLangfuse ya no almacena una contraseña tuya. Y ahora tienes un proveedor al que puedes apuntar\nla siguiente app en unos cinco minutos.</p>\n<p>No hecho, y vale la pena ser claro al respecto:</p>\n<ul>\n<li class=\"\"><strong>Sin control de acceso.</strong> Vinculaciones vacías significan que toda cuenta de authentik\nllega a Langfuse.</li>\n<li class=\"\"><strong>HTTP plano.</strong> El token de ID y el secreto del cliente cruzan la red sin cifrar. En una LAN\nde confianza eso es un punto de partida tolerable; expuesto a cualquier otra cosa no lo es.</li>\n<li class=\"\"><strong>Un administrador, sin vía de recuperación.</strong> Si pierdes <code>akadmin</code>, pierdes el proveedor que\nhace de fachada de todo lo que apunte a él.</li>\n<li class=\"\"><strong>El login antiguo sigue funcionando.</strong> El acceso por contraseña no se deshabilitó, así que\nel SSO es por ahora una puerta adicional, no una de reemplazo.</li>\n</ul>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Identity in front of every port</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-sigue\">Qué sigue<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al Qué sigue\" title=\"Enlace directo al Qué sigue\" translate=\"no\">​</a></h2>\n<p>La Parte 4 pone identidad delante de algo donde estos huecos dejan de ser tolerables: la\ngateway LiteLLM de la <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/\">serie del gateway</a>. Tres\ncosas que este post dejó deliberadamente en paz:</p>\n<ul>\n<li class=\"\"><strong>El plano de control no es el plano de datos.</strong> El SSO protege una interfaz de\nadministración. No protege una API — los llamantes máquina siguen autenticándose con claves.\nPara un gateway esa distinción es todo el juego, y suponer lo contrario es cómo la gente\nconcluye que ha asegurado algo que no ha asegurado.</li>\n<li class=\"\"><strong>Grupos y vinculaciones de políticas</strong>, para que alcanzar la aplicación requiera pertenencia\ny no meramente tener una cuenta.</li>\n<li class=\"\"><strong>TLS y exposición real</strong> — el gateway está enlazado a <code>127.0.0.1:4000</code>, y hacerlo\ncorrectamente accesible significa un proxy inverso, un nombre de host y un certificado.</li>\n</ul>\n<p>Los pasos de proveedor y aplicación serán un repaso corto con un enlace de vuelta aquí. El\nresto es nuevo.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lecturas-adicionales\">Lecturas adicionales<a href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/#lecturas-adicionales\" class=\"hash-link\" aria-label=\"Enlace directo al Lecturas adicionales\" title=\"Enlace directo al Lecturas adicionales\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.goauthentik.io/docs/add-secure-apps/providers/oauth2/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Documentación de authentik — proveedor OAuth2/OpenID</a></li>\n<li class=\"\"><a href=\"https://docs.goauthentik.io/docs/add-secure-apps/applications/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Documentación de authentik — aplicaciones, y vincularles políticas</a></li>\n<li class=\"\"><a href=\"https://langfuse.com/self-hosting/authentication-and-sso\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Documentación de Langfuse — SSO y las variables <code>AUTH_CUSTOM_*</code></a></li>\n<li class=\"\"><a href=\"https://next-auth.js.org/configuration/providers/oauth#allowdangerousemailaccountlinking-option\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">NextAuth — vinculación de cuentas, y por qué existe <code>OAuthAccountNotLinked</code></a></li>\n<li class=\"\"><a href=\"https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">OpenID Connect Core — el flujo de código de autorización</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/",
            "title": "Un solo inicio de sesión para todo: poniendo authentik delante de una app autoalojada",
            "summary": "La Parte 2 arregló cómo se ejecutan los contenedores. Esta arregla quién puede iniciar sesión. Despliega authentik como tu propio proveedor de identidad, conecta Langfuse a él por OIDC, y entiende las piezas — proveedor, aplicación, URI de redirección, scopes — en lugar de copiar una configuración. Incluye los dos fallos que produjo una ejecución real: un archivo compose que se traga tus ajustes en silencio, y OAuthAccountNotLinked.",
            "date_modified": "2026-09-02T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "seguridad",
                "sso",
                "oidc",
                "authentik",
                "identidad",
                "langfuse",
                "autoalojamiento",
                "endurecimiento"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/load-test-llm-gateway-blocking-callback/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/litellm-wordmark-86d6f0432720b27534e05bc579dfc29c.png\" alt=\"LiteLLM\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__presidio\" src=\"data:image/webp;base64,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\" alt=\"Presidio\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 5 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->5</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->5<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting an LLM gateway</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">One endpoint, scoped keys</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Route work to the right model</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Mask PII at the gateway</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Clean traces, untouched answers</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">🏆</span><span class=\"skillTracker__skill\" data-state=\"current\">Prove it holds under load</span></span></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>La <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/\">Parte 4</a> construyó un callback\nque enmascara la PII en lo que el gateway registra sin tocar lo que recibe quien llama.\nTambién señaló que la primera versión funcional usaba un cliente HTTP bloqueante dentro\nde un <code>async def</code>, y que eso costaba 200-350ms sobre cuatro peticiones secuenciales.</p>\n<p>Ese post terminaba con una confesión: cuatro peticiones seguidas es la prueba equivocada.\nUn bucle de eventos bloqueado apenas se nota cuando nada más está esperando. El daño\ndebería aparecer bajo concurrencia, y no lo habíamos medido.</p>\n<p>Esta es esa medición, y el resultado es una forma más que un número. <strong>Con el cliente\nbloqueante, algunos lotes tardan muchas veces más de lo que deberían. Con el cliente\nasíncrono, ninguno.</strong> Las medianas apenas difieren, que es exactamente la razón por la\nque esto llega a producción sin que nadie lo note.</p>\n<p>Aparecieron dos cosas que no estaban en el plan. El gateway no solo se vuelve lento —\n<strong>descarta peticiones</strong>. Y cuando lo hace, lo que expiró fue la llamada de enmascarado,\nmientras quien llama sigue recibiendo <code>200 OK</code>. Eso significa que la garantía sobre la\nque se construyó la Parte 4 deja de cumplirse en silencio bajo carga.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-necesitas\">Lo que necesitas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/load-test-llm-gateway-blocking-callback/#lo-que-necesitas\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que necesitas\" title=\"Enlace directo al Lo que necesitas\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">El gateway de la <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/\">Parte 1</a>, en marcha</li>\n<li class=\"\">Presidio y el callback de registro de la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/\">Parte 4</a></li>\n<li class=\"\">Una clave de la API de Inferencia de WEC — <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/cloud_portal/platform/inference/api_keys/\">créala</a>\nen el portal</li>\n<li class=\"\"><code>httpx</code> en tu entorno de Python</li>\n</ul>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>De dónde salieron estos números</div><div class=\"admonitionContent_BuS1\"><p>Una instancia WEC: 8 vCPU AMD EPYC 7601, 15 GB de RAM, Docker 29.1.3, kernel 5.15.\nLiteLLM <code>1.96.2</code> desde <code>ghcr.io/berriai/litellm:main-stable</code>. El analizador y el\nanonimizador de Presidio como contenedores locales en el mismo host. Modelo\n<code>Qwen2.5-3B-Instruct</code> sobre la API de Inferencia de WEC. Cinco repeticiones por celda.</p><p>Los números de latencia no se transfieren entre máquinas. Trata cada segundo de este post\ncomo una observación de ese entorno, no como una cifra que igualar. Lo que sí debería\nreproducirse es la diferencia entre las dos versiones.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"primero-averigua-cuántos-bucles-de-eventos-tienes\">Primero, averigua cuántos bucles de eventos tienes<a href=\"https://development-wec.wiline.com/docs/es/tutorials/load-test-llm-gateway-blocking-callback/#primero-averigua-cu%C3%A1ntos-bucles-de-eventos-tienes\" class=\"hash-link\" aria-label=\"Enlace directo al Primero, averigua cuántos bucles de eventos tienes\" title=\"Enlace directo al Primero, averigua cuántos bucles de eventos tienes\" translate=\"no\">​</a></h2>\n<p>Todo lo que sigue depende de un número, y no es el número que da la documentación.</p>\n<p>La CLI del proxy de LiteLLM tiene una opción <code>--num_workers</code>. La\n<a href=\"https://docs.litellm.ai/docs/proxy/cli\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">referencia publicada</a> dice que su valor por\ndefecto es <em>\"Number of logical CPUs in the system, or 4 if that cannot be determined.\"</em>\nEn una máquina de ocho núcleos eso significaría ocho procesos de trabajo, ocho bucles de\neventos, y una llamada bloqueante atascando una octava parte de tu tráfico.</p>\n<p>Pregúntale al host qué se está ejecutando realmente — desde fuera del contenedor, para\nque nada dentro de él pueda informar mal:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">top</span><span class=\"token plain\"> llm-gateway</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">UID    PID       PPID      C  STIME  TTY  TIME      CMD</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">root   2477458   2477435   3  16:43  ?    00:01:19  /app/.venv/bin/python3 /app/.venv/bin/litellm --config /app/config.yaml --port 4000</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">root   2481091   2477458   0  16:44  ?    00:00:05  /opt/prisma/binaries/node_modules/prisma/query-engine-debian-openssl-3.0.x -p 53053</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker top mostrando un proceso de python de litellm y un motor de consultas de Prisma\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw5-one-process-a28a66c6eafcb70fa88a41c347ddd22d.png\" width=\"643\" height=\"182\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figura 1.</strong> Un proceso de python ejecuta todo el proxy. La segunda entrada es Prisma, el\ncliente de base de datos, que no atiende peticiones.</p>\n<p>Si <code>--num_workers</code> fuera mayor que uno verías varios procesos de python con el primero\ncomo padre. Confírmalo en el registro de arranque:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> logs llm-gateway </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-E</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Started server process|Uvicorn running\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-2</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">INFO:     Started server process [1]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">INFO:     Uvicorn running on http://0.0.0.0:4000 (Press CTRL+C to quit)</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El registro de arranque mostrando un único proceso de servidor y uvicorn en el puerto 4000\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw5-startup-log-df1d1c66b00b54c6d3cca0e6787ae4eb.png\" width=\"645\" height=\"87\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figura 2.</strong> Un proceso de servidor. Este registro se acumula entre reinicios, así que\n<code>tail -2</code> te mantiene mirando el actual.</p>\n<p>Ahora pregúntale a la herramienta cuál es su valor por defecto:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> llm-gateway litellm </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--help</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-A5</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"^  --num_workers\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --num_workers INTEGER           Number of worker processes for uvicorn /</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                  gunicorn, or Granian worker processes</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                  (--workers). Default is 1 (from</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                  DEFAULT_NUM_WORKERS_LITELLM_PROXY). With</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                  --run_granian, use --granian_threads for</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                  runtime threads per worker.</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La ayuda de litellm con la frase Default is 1 resaltada\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw5-worker-default-a862f11836711c637727b9c24b5d3b0a.png\" width=\"922\" height=\"125\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figura 3.</strong> El propio texto de ayuda de la herramienta: <em>\"Default is 1.\"</em></p>\n<p>Eso es una cadena de ayuda. Como estamos a punto de contradecir la documentación\npublicada, tómalo también del código fuente — ajusta la versión de python en la ruta para\nque coincida con tu imagen:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> llm-gateway </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-rn</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'DEFAULT_NUM_WORKERS_LITELLM_PROXY'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  /app/.venv/lib/python3.13/site-packages/litellm/constants.py</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">15:DEFAULT_NUM_WORKERS_LITELLM_PROXY = int(os.getenv(\"DEFAULT_NUM_WORKERS_LITELLM_PROXY\", 1))</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>La referencia publicada se equivoca en esto</div><div class=\"admonitionContent_BuS1\"><p>La <a href=\"https://docs.litellm.ai/docs/proxy/cli\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">referencia de la CLI</a> indica que el valor por\ndefecto de <code>--num_workers</code> es <em>\"Number of logical CPUs in the system, or 4 if that cannot\nbe determined.\"</em> Tres fuentes dicen lo contrario: el contenedor en marcha, el código\ninstalado de <code>1.96.2</code>, y el <code>main</code> actual del proyecto, donde tanto la constante como el\ntexto de ayuda siguen diciendo <strong>1</strong>.</p><p>No es un desajuste de versiones, y no es algo de este despliegue. Comprueba el tuyo:</p><div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> llm-gateway </span><span class=\"token function\" style=\"color:#d73a49\">env</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> worker</span><br></div></code></pre></div></div><p>Una salida vacía significa que estás en el valor por defecto, que es un worker.</p></div></div>\n<p>Así que: un proceso, un bucle de eventos, compartido por cada petición en vuelo. Esa es la\ncondición que convierte una llamada bloqueante de un impuesto en una caída.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"un-script-de-carga\">Un script de carga<a href=\"https://development-wec.wiline.com/docs/es/tutorials/load-test-llm-gateway-blocking-callback/#un-script-de-carga\" class=\"hash-link\" aria-label=\"Enlace directo al Un script de carga\" title=\"Enlace directo al Un script de carga\" translate=\"no\">​</a></h2>\n<p>Lanza N peticiones a la vez, cronometra cada una, informa el tiempo total del lote. El\nmensaje lleva PII para que el callback de enmascarado tenga trabajo real. Una petición\nfallida se cuenta en vez de dejar que abandone la ejecución — vas a necesitar eso.</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/llm-gateway/loadtest.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"Fire N chat completions at the gateway at once and report how long they take.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"display:inline-block;color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">The message carries PII so the Presidio masking callback has real work to do.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">A failed request is counted and reported, not allowed to abandon the batch.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> asyncio</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> os</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> statistics</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> sys</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> time</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> httpx</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">GATEWAY </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://127.0.0.1:4000/v1/chat/completions\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">KEY </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"LITELLM_MASTER_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">MODEL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"qwen-small\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">PROMPT </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"Priya Raghunathan called from 415-555-0134 about her invoice. \"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"Reply with exactly: ok\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">one</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">client</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> i</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    start </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">perf_counter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">try</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        r </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">post</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            GATEWAY</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            headers</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Bearer </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">KEY</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            json</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string\" style=\"color:#e3116c\">\"model\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> MODEL</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"role\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"user\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> PROMPT</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string\" style=\"color:#e3116c\">\"max_tokens\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">5</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">except</span><span class=\"token plain\"> Exception </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> exc</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> i</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\">type</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">exc</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">__name__</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> i</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">status_code</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">perf_counter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token plain\"> start</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    n </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token builtin\">int</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> httpx</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">AsyncClient</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">timeout</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">120.0</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        wall </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">perf_counter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        results </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">gather</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token operator\" style=\"color:#393A34\">*</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">one</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">client</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> i</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> i </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token builtin\">range</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">n</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        wall </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">perf_counter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token plain\"> wall</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    times </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token builtin\">sorted</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">e </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> _</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> _</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> e</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> _ </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> results </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> e </span><span class=\"token keyword\" style=\"color:#00009f\">is</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    errors </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">err </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> _</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> _</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> _</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> err </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> results </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> err</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"concurrency </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">n</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">  ok </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation builtin\">len</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation\">times</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">  failed </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation builtin\">len</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation\">errors</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> errors</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"  errors   </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">', '</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">join</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation builtin\">sorted</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation builtin\">set</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation\">errors</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"  wall     </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">wall</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">:</span><span class=\"token string-interpolation interpolation format-spec\">.2f</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">s\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> times</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"  fastest  </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">times</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation number\" style=\"color:#36acaa\">0</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">:</span><span class=\"token string-interpolation interpolation format-spec\">.2f</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">s\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"  median   </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">statistics</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">median</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation\">times</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">:</span><span class=\"token string-interpolation interpolation format-spec\">.2f</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">s\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"  slowest  </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">times</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation operator\" style=\"color:#393A34\">-</span><span class=\"token string-interpolation interpolation number\" style=\"color:#36acaa\">1</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">:</span><span class=\"token string-interpolation interpolation format-spec\">.2f</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">s\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/llm-gateway </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-a</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">.</span><span class=\"token plain\"> ./.env </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> +a</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python loadtest.py </span><span class=\"token number\" style=\"color:#36acaa\">8</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">concurrency 8  ok 8  failed 0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  wall     1.93s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  fastest  1.60s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  median   1.91s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  slowest  1.93s</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Ocho peticiones concurrentes por el gateway, todas con éxito, 1,93 segundos de lote\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw5-first-load-be7eeba783710fd2f310b7add1c21fdc.png\" width=\"645\" height=\"120\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figura 4.</strong> Ocho a la vez, ninguna falló. Por sí solo este número todavía no significa\nnada.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"necesitas-un-control-o-tus-números-no-significan-nada\">Necesitas un control, o tus números no significan nada<a href=\"https://development-wec.wiline.com/docs/es/tutorials/load-test-llm-gateway-blocking-callback/#necesitas-un-control-o-tus-n%C3%BAmeros-no-significan-nada\" class=\"hash-link\" aria-label=\"Enlace directo al Necesitas un control, o tus números no significan nada\" title=\"Enlace directo al Necesitas un control, o tus números no significan nada\" translate=\"no\">​</a></h2>\n<p>Aquí está la trampa. El gateway llama a un modelo por la red. Ese modelo tiene su propia\nlatencia, su propia carga y sus propias malas tardes. Cuando un lote tarda doce segundos\nno puedes saber si tu gateway se atascó o si el upstream estaba ocupado — y si adivinas,\nvas a publicar una tontería.</p>\n<p>Así que mide el upstream directamente, con el gateway fuera del camino. Copia el archivo y\ncambia las tres constantes de arriba:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/llm-gateway</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">cp</span><span class=\"token plain\"> loadtest.py loadtest_direct.py</span><br></div></code></pre></div></div>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/llm-gateway/loadtest_direct.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">GATEWAY </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"https://inference.wiline.com/v1/chat/completions\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">KEY </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"WEC_API_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">MODEL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Qwen2.5-3B-Instruct\"</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python loadtest_direct.py </span><span class=\"token number\" style=\"color:#36acaa\">8</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">concurrency 8  ok 8  failed 0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  wall     1.15s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  fastest  1.05s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  median   1.10s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  slowest  1.15s</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Ocho peticiones concurrentes directas al upstream, 1,15 segundos de lote\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw5-control-6c9353892f98ded0047bab740eae67a0.png\" width=\"643\" height=\"126\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figura 5.</strong> Las mismas ocho peticiones con el gateway quitado. Esta es la referencia\ncontra la que se lee cada número del gateway.</p>\n<p>Ahora ejecuta las dos <strong>intercaladas</strong>, no una después de la otra, para que un minuto\nlento del upstream no caiga entero sobre una sola versión:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/llm-gateway/run_experiment.sh</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token shebang important\">#!/bin/bash</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Interleave gateway runs with direct-to-upstream control runs so an upstream</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># slowdown cannot be mistaken for a gateway effect.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> -a</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">.</span><span class=\"token plain\"> ./.env</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> +a</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">REPS</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">5</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># A restart takes longer than it looks. Firing requests at a port that is not</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># answering yet produces instant failures that have nothing to do with the callback.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">printf</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'waiting for the gateway'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> </span><span class=\"token for-or-select variable\" style=\"color:#36acaa\">_</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">seq</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable number\" style=\"color:#36acaa\">1</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable number\" style=\"color:#36acaa\">60</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sf</span><span class=\"token plain\"> http://127.0.0.1:4000/health/liveliness </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">break</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token builtin class-name\">printf</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.'</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">done</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sf</span><span class=\"token plain\"> http://127.0.0.1:4000/health/liveliness </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">||</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\" never came up — aborting\"</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exit</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\" up\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Do not trust a label passed on the command line — ask the gateway which callback</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># it actually loaded. A mislabelled run is worse than no run.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">LABEL</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">docker</span><span class=\"token variable\" style=\"color:#36acaa\"> logs llm-gateway </span><span class=\"token variable operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token variable operator\" style=\"color:#393A34\">&gt;</span><span class=\"token variable file-descriptor important\" style=\"color:#36acaa\">&amp;1</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">grep</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'\\[scrubber\\]'</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">tail</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-1</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">sed</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'s/.*loaded: //'</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-z</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$LABEL</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">then</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"cannot determine loaded callback — aborting\"</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exit</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">fi</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"callback in use: </span><span class=\"token string variable\" style=\"color:#36acaa\">$LABEL</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function-name function\" style=\"color:#d73a49\">summarise</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">awk</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'/^concurrency/{failed=$6} /wall/{wall=$2} END{printf \"%s\", wall; if (failed+0 &gt; 0) printf \"(%d failed)\", failed}'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># The first request after a restart pays for imports and connection pools, which</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># has nothing to do with the callback. Warm both paths before recording anything.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python loadtest.py </span><span class=\"token number\" style=\"color:#36acaa\">4</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python loadtest_direct.py </span><span class=\"token number\" style=\"color:#36acaa\">4</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> </span><span class=\"token for-or-select variable\" style=\"color:#36acaa\">n</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">16</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> </span><span class=\"token for-or-select variable\" style=\"color:#36acaa\">i</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">seq</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable number\" style=\"color:#36acaa\">1</span><span class=\"token variable\" style=\"color:#36acaa\"> $REPS</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">g</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/.venv/bin/python loadtest.py $n </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> summarise</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">d</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/.venv/bin/python loadtest_direct.py $n </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> summarise</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$LABEL</span><span class=\"token string\" style=\"color:#e3116c\"> N=</span><span class=\"token string variable\" style=\"color:#36acaa\">$n</span><span class=\"token string\" style=\"color:#e3116c\"> rep=</span><span class=\"token string variable\" style=\"color:#36acaa\">$i</span><span class=\"token string\" style=\"color:#e3116c\"> gateway=</span><span class=\"token string variable\" style=\"color:#36acaa\">$g</span><span class=\"token string\" style=\"color:#e3116c\"> direct=</span><span class=\"token string variable\" style=\"color:#36acaa\">$d</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"color:#00009f\">done</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">done</span><br></div></code></pre></div></div>\n<p>Tres detalles de ahí no son decoración, y cada uno costó una ejecución perdida para\naprenderlo:</p>\n<p><strong>Espera al gateway.</strong> Un <code>docker compose restart</code> puede tardar más de diez segundos, y\nlanzar peticiones a un puerto que todavía no escucha produce fallos instantáneos —\n<code>gateway=0.03s(8 failed)</code> — que parecen un resultado catastrófico y no significan nada.</p>\n<p><strong>Se niega a aceptar una etiqueta tuya.</strong> Lee el callback cargado del registro del\ngateway. Pasa <code>blocking</code> por la línea de comandos mientras está cargado el callback\nasíncrono y vas a medir el mismo código dos veces, no ver diferencia, y concluir que aquí\nno hay nada. Eso pasó dos veces escribiendo este post.</p>\n<p><strong>Descarta una pasada de calentamiento.</strong> El primer lote tras un reinicio paga las\nimportaciones y los pools de conexión, y llega de tres a seis veces más lento que el\nsiguiente, sin importar qué callback esté cargado.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"haz-que-cada-versión-se-anuncie\">Haz que cada versión se anuncie<a href=\"https://development-wec.wiline.com/docs/es/tutorials/load-test-llm-gateway-blocking-callback/#haz-que-cada-versi%C3%B3n-se-anuncie\" class=\"hash-link\" aria-label=\"Enlace directo al Haz que cada versión se anuncie\" title=\"Enlace directo al Haz que cada versión se anuncie\" translate=\"no\">​</a></h2>\n<p>Estás a punto de comparar dos versiones de un archivo, y necesitas certeza sobre cuál está\nactiva. El registro de arranque de LiteLLM lista los callbacks de éxito y de fallo pero no\n<code>litellm_settings.callbacks</code>, y no hay un endpoint que los informe —\n<code>/get/config/callbacks</code> responde <code>200</code> con <code>{\"detail\":\"Not Found\"}</code>.</p>\n<p>Así que haz que cada versión diga su propio nombre al importarse, donde no cuesta nada por\npetición:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># last line of scrubber.py</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"[scrubber] loaded: async httpx.AsyncClient\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># last line of scrubber_blocking.py</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"[scrubber] loaded: blocking httpx.Client\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Monta los dos archivos y cambia con una línea de configuración:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">docker-compose.yml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">volumes</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> ./config.yaml</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/app/config.yaml</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">ro</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> ./scrubber.py</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/app/scrubber.py</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">ro</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> ./scrubber_blocking.py</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/app/scrubber_blocking.py</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">ro</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> ./recognizers.json</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/app/recognizers.json</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">ro</span><br></div></code></pre></div></div>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">litellm_settings</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">callbacks</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scrubber.instance\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\">          </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># or scrubber_blocking.instance</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sed</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'s/scrubber.instance/scrubber_blocking.instance/'</span><span class=\"token plain\"> config.yaml</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose restart litellm</span><br></div></code></pre></div></div>\n<p>El script del experimento imprime el callback cargado como primera línea, así que cada\nejecución lleva su propia prueba de lo que midió.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"los-números\">Los números<a href=\"https://development-wec.wiline.com/docs/es/tutorials/load-test-llm-gateway-blocking-callback/#los-n%C3%BAmeros\" class=\"hash-link\" aria-label=\"Enlace directo al Los números\" title=\"Enlace directo al Los números\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">./run_experiment.sh</span><br></div></code></pre></div></div>\n<p>Con el cliente asíncrono:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">callback in use: async httpx.AsyncClient</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=8 rep=1 gateway=0.98s direct=1.09s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=8 rep=2 gateway=0.86s direct=0.80s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=8 rep=3 gateway=0.92s direct=0.96s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=8 rep=4 gateway=0.78s direct=0.85s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=8 rep=5 gateway=0.90s direct=0.82s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=16 rep=1 gateway=1.61s direct=1.49s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=16 rep=2 gateway=1.65s direct=1.26s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=16 rep=3 gateway=1.28s direct=1.35s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=16 rep=4 gateway=1.33s direct=1.42s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=16 rep=5 gateway=1.77s direct=1.56s</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Diez ejecuciones intercaladas con el callback asíncrono, siguiendo al control en cada fila\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw5-async-runs-44f396a0682d60c0748426e50abc69cf.png\" width=\"508\" height=\"233\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figura 6.</strong> El cliente asíncrono. Cada número del gateway está junto a su control, en\nlos dos niveles de concurrencia. Nada destaca porque nada pasó.</p>\n<p>Después cambia el callback y ejecuta lo mismo:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">waiting for the gateway up</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">callback in use: blocking httpx.Client</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=8 rep=1 gateway=1.20s direct=0.93s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=8 rep=2 gateway=0.96s direct=0.83s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=8 rep=3 gateway=0.92s direct=0.86s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=8 rep=4 gateway=0.73s direct=0.90s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=8 rep=5 gateway=1.70s direct=0.90s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=16 rep=1 gateway=70.18s direct=2.00s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=16 rep=2 gateway=1.75s direct=1.70s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=16 rep=3 gateway=2.33s direct=1.78s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=16 rep=4 gateway=2.88s direct=1.49s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=16 rep=5 gateway=5.14s direct=1.19s</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Diez ejecuciones con el callback bloqueante, un lote en 70,18 segundos y otro en 5,14 frente a controles de 1,2 a 2 segundos\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw5-blocking-runs-df9535bdd0ed417cb65599e60ca5e742.png\" width=\"512\" height=\"239\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figura 7.</strong> El cliente bloqueante. Mismo script, misma carga, una palabra distinta en el\ncódigo. La columna de control se mantiene entre 1,19 y 2,00s en todo momento.</p>\n<table><thead><tr><th>Versión</th><th>N</th><th>Mediana del gateway</th><th>Peor del gateway</th><th>Mediana del control</th></tr></thead><tbody><tr><td>asíncrono <code>AsyncClient</code></td><td>8</td><td>0,90s</td><td>0,98s</td><td>0,85s</td></tr><tr><td>asíncrono <code>AsyncClient</code></td><td>16</td><td>1,61s</td><td>1,77s</td><td>1,42s</td></tr><tr><td>bloqueante <code>Client</code></td><td>8</td><td>0,96s</td><td>1,70s</td><td>0,90s</td></tr><tr><td>bloqueante <code>Client</code></td><td>16</td><td>2,88s</td><td><strong>70,18s</strong> *</td><td>1,70s</td></tr></tbody></table>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Gráfico de puntos en escala logarítmica de cada lote. Los puntos grises del control se agrupan entre 0,7 y 2 segundos en los cuatro grupos. Los verdes asíncronos están entre ellos. Dos puntos rojos bloqueantes se separan en 5,14 y 70,18 segundos\" src=\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3NjAgNDQwIiB3aWR0aD0iNzYwIiBoZWlnaHQ9IjQ0MCIgZm9udC1mYW1pbHk9InN5c3RlbS11aSwtYXBwbGUtc3lzdGVtLFNlZ29lIFVJLHNhbnMtc2VyaWYiPgo8dGl0bGU+V2FsbCB0aW1lIHBlciBiYXRjaCwgdGhyb3VnaCB0aGUgZ2F0ZXdheSB2ZXJzdXMgc3RyYWlnaHQgdG8gdGhlIHVwc3RyZWFtPC90aXRsZT4KPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9Ijc2MCIgaGVpZ2h0PSI0NDAiIGZpbGw9IiNmZmZmZmYiLz4KPGxpbmUgeDE9Ijg2IiB5MT0iMzUyLjAiIHgyPSI3MzAiIHkyPSIzNTIuMCIgc3Ryb2tlPSIjOTRhM2I4IiBzdHJva2Utb3BhY2l0eT0iMC4zMCIvPgo8dGV4dCB4PSI3NCIgeT0iMzU2LjAiIHRleHQtYW5jaG9yPSJlbmQiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiM2NDc0OGIiPjAuNXM8L3RleHQ+CjxsaW5lIHgxPSI4NiIgeTE9IjMxMS43IiB4Mj0iNzMwIiB5Mj0iMzExLjciIHN0cm9rZT0iIzk0YTNiOCIgc3Ryb2tlLW9wYWNpdHk9IjAuMzAiLz4KPHRleHQgeD0iNzQiIHk9IjMxNS43IiB0ZXh0LWFuY2hvcj0iZW5kIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNjQ3NDhiIj4xczwvdGV4dD4KPGxpbmUgeDE9Ijg2IiB5MT0iMjcxLjQiIHgyPSI3MzAiIHkyPSIyNzEuNCIgc3Ryb2tlPSIjOTRhM2I4IiBzdHJva2Utb3BhY2l0eT0iMC4zMCIvPgo8dGV4dCB4PSI3NCIgeT0iMjc1LjQiIHRleHQtYW5jaG9yPSJlbmQiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiM2NDc0OGIiPjJzPC90ZXh0Pgo8bGluZSB4MT0iODYiIHkxPSIyMTguMSIgeDI9IjczMCIgeTI9IjIxOC4xIiBzdHJva2U9IiM5NGEzYjgiIHN0cm9rZS1vcGFjaXR5PSIwLjMwIi8+Cjx0ZXh0IHg9Ijc0IiB5PSIyMjIuMSIgdGV4dC1hbmNob3I9ImVuZCIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzY0NzQ4YiI+NXM8L3RleHQ+CjxsaW5lIHgxPSI4NiIgeTE9IjE3Ny45IiB4Mj0iNzMwIiB5Mj0iMTc3LjkiIHN0cm9rZT0iIzk0YTNiOCIgc3Ryb2tlLW9wYWNpdHk9IjAuMzAiLz4KPHRleHQgeD0iNzQiIHk9IjE4MS45IiB0ZXh0LWFuY2hvcj0iZW5kIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNjQ3NDhiIj4xMHM8L3RleHQ+CjxsaW5lIHgxPSI4NiIgeTE9IjEzNy42IiB4Mj0iNzMwIiB5Mj0iMTM3LjYiIHN0cm9rZT0iIzk0YTNiOCIgc3Ryb2tlLW9wYWNpdHk9IjAuMzAiLz4KPHRleHQgeD0iNzQiIHk9IjE0MS42IiB0ZXh0LWFuY2hvcj0iZW5kIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNjQ3NDhiIj4yMHM8L3RleHQ+CjxsaW5lIHgxPSI4NiIgeTE9Ijg0LjMiIHgyPSI3MzAiIHkyPSI4NC4zIiBzdHJva2U9IiM5NGEzYjgiIHN0cm9rZS1vcGFjaXR5PSIwLjMwIi8+Cjx0ZXh0IHg9Ijc0IiB5PSI4OC4zIiB0ZXh0LWFuY2hvcj0iZW5kIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNjQ3NDhiIj41MHM8L3RleHQ+CjxsaW5lIHgxPSI4NiIgeTE9IjQ0LjAiIHgyPSI3MzAiIHkyPSI0NC4wIiBzdHJva2U9IiM5NGEzYjgiIHN0cm9rZS1vcGFjaXR5PSIwLjMwIi8+Cjx0ZXh0IHg9Ijc0IiB5PSI0OC4wIiB0ZXh0LWFuY2hvcj0iZW5kIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNjQ3NDhiIj4xMDBzPC90ZXh0Pgo8Y2lyY2xlIGN4PSIxNDAuNSIgY3k9IjMwNi43IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIxNDAuNSIgY3k9IjMyNC43IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIxNDAuNSIgY3k9IjMxNC4xIiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIxNDAuNSIgY3k9IjMyMS4yIiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIxNDAuNSIgY3k9IjMyMy4yIiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIxOTIuNSIgY3k9IjMxMi45IiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSIxOTIuNSIgY3k9IjMyMC41IiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSIxOTIuNSIgY3k9IjMxNi42IiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSIxOTIuNSIgY3k9IjMyNi4xIiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSIxOTIuNSIgY3k9IjMxNy44IiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8dGV4dCB4PSIxNjYuNSIgeT0iMzc2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNjQ3NDhiIj5hc3luYzwvdGV4dD4KPHRleHQgeD0iMTY2LjUiIHk9IjM5MyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzY0NzQ4YiI+Tj04PC90ZXh0Pgo8Y2lyY2xlIGN4PSIzMDEuNSIgY3k9IjMxNS45IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIzMDEuNSIgY3k9IjMyMi41IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIzMDEuNSIgY3k9IjMyMC41IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIzMDEuNSIgY3k9IjMxNy44IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIzMDEuNSIgY3k9IjMxNy44IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIzNTMuNSIgY3k9IjMwMS4xIiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSIzNTMuNSIgY3k9IjMxNC4xIiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSIzNTMuNSIgY3k9IjMxNi42IiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSIzNTMuNSIgY3k9IjMzMC4wIiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSIzNTMuNSIgY3k9IjI4MC45IiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8dGV4dCB4PSIzMjcuNSIgeT0iMzc2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNjQ3NDhiIj5ibG9ja2luZzwvdGV4dD4KPHRleHQgeD0iMzI3LjUiIHk9IjM5MyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzY0NzQ4YiI+Tj04PC90ZXh0Pgo8Y2lyY2xlIGN4PSI0NjIuNSIgY3k9IjI4OC41IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSI0NjIuNSIgY3k9IjI5OC4zIiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSI0NjIuNSIgY3k9IjI5NC4zIiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSI0NjIuNSIgY3k9IjI5MS4zIiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSI0NjIuNSIgY3k9IjI4NS45IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSI1MTQuNSIgY3k9IjI4NC4wIiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSI1MTQuNSIgY3k9IjI4Mi42IiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSI1MTQuNSIgY3k9IjI5Ny40IiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSI1MTQuNSIgY3k9IjI5NS4xIiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSI1MTQuNSIgY3k9IjI3OC41IiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8dGV4dCB4PSI0ODguNSIgeT0iMzc2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNjQ3NDhiIj5hc3luYzwvdGV4dD4KPHRleHQgeD0iNDg4LjUiIHk9IjM5MyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzY0NzQ4YiI+Tj0xNjwvdGV4dD4KPGNpcmNsZSBjeD0iNjIzLjUiIGN5PSIyNzEuNCIgcj0iNC41IiBmaWxsPSIjOTRhM2I4IiBmaWxsLW9wYWNpdHk9IjAuODUiLz4KPGNpcmNsZSBjeD0iNjIzLjUiIGN5PSIyODAuOSIgcj0iNC41IiBmaWxsPSIjOTRhM2I4IiBmaWxsLW9wYWNpdHk9IjAuODUiLz4KPGNpcmNsZSBjeD0iNjIzLjUiIGN5PSIyNzguMiIgcj0iNC41IiBmaWxsPSIjOTRhM2I4IiBmaWxsLW9wYWNpdHk9IjAuODUiLz4KPGNpcmNsZSBjeD0iNjIzLjUiIGN5PSIyODguNSIgcj0iNC41IiBmaWxsPSIjOTRhM2I4IiBmaWxsLW9wYWNpdHk9IjAuODUiLz4KPGNpcmNsZSBjeD0iNjIzLjUiIGN5PSIzMDEuNiIgcj0iNC41IiBmaWxsPSIjOTRhM2I4IiBmaWxsLW9wYWNpdHk9IjAuODUiLz4KPGNpcmNsZSBjeD0iNjc1LjUiIGN5PSI2NC42IiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSI2NzUuNSIgY3k9IjI3OS4yIiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSI2NzUuNSIgY3k9IjI2Mi41IiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSI2NzUuNSIgY3k9IjI1MC4yIiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSI2NzUuNSIgY3k9IjIxNi41IiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8dGV4dCB4PSI2NDkuNSIgeT0iMzc2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNjQ3NDhiIj5ibG9ja2luZzwvdGV4dD4KPHRleHQgeD0iNjQ5LjUiIHk9IjM5MyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzY0NzQ4YiI+Tj0xNjwvdGV4dD4KPHRleHQgeD0iNjg3LjUiIHk9IjY4LjYiIGZvbnQtc2l6ZT0iMTMiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiNkYzI2MjYiPjcwLjE4cyAqPC90ZXh0Pgo8dGV4dCB4PSI2ODcuNSIgeT0iMjIwLjUiIGZvbnQtc2l6ZT0iMTMiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiNkYzI2MjYiPjUuMTRzPC90ZXh0Pgo8Y2lyY2xlIGN4PSI5NCIgY3k9IjI4IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiLz4KPHRleHQgeD0iMTA2IiB5PSIzMiIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzY0NzQ4YiI+c3RyYWlnaHQgdG8gdGhlIHVwc3RyZWFtIChjb250cm9sKTwvdGV4dD4KPGNpcmNsZSBjeD0iMzg2IiBjeT0iMjgiIHI9IjUiIGZpbGw9IiMwNTk2NjkiLz4KPHRleHQgeD0iMzk4IiB5PSIzMiIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzY0NzQ4YiI+Z2F0ZXdheSwgYXN5bmM8L3RleHQ+CjxjaXJjbGUgY3g9IjUzNiIgY3k9IjI4IiByPSI1IiBmaWxsPSIjZGMyNjI2Ii8+Cjx0ZXh0IHg9IjU0OCIgeT0iMzIiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiM2NDc0OGIiPmdhdGV3YXksIGJsb2NraW5nPC90ZXh0Pgo8dGV4dCB4PSI4NiIgeT0iNDI4IiBmb250LXNpemU9IjEyIiBmaWxsPSIjNjQ3NDhiIj4qIHRoaXMgYmF0Y2ggb3ZlcmxhcHBlZCBhbiB1bnJlbGF0ZWQgbG9hZCB0ZXN0IG9uIHRoZSBzYW1lIGhvc3Qg4oCUIHRyZWF0IGl0IGFzIGFuIHVwcGVyIGJvdW5kPC90ZXh0Pgo8L3N2Zz4=\" width=\"760\" height=\"440\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figura 8.</strong> Cada lote de las Figuras 6 y 7 en un eje logarítmico. El control se mantiene\nen una banda estrecha en los cuatro grupos. Las ejecuciones asíncronas se quedan con él.\nDos bloqueantes lo abandonan.</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>Un número de esa tabla necesita un asterisco</div><div class=\"admonitionContent_BuS1\"><p>El lote de 70,18s coincidió con otra prueba de carga no relacionada contra el mismo\ngateway, así que parte de ese tiempo no es atribuible al callback. Se informa porque\nocurrió, no porque esté limpio. Lee el lote de 5,14s como el atasco representativo —\ntodavía más de cuatro veces su propio control de 1,19s.</p><p>El efecto en sí se reprodujo en cuatro ejecuciones distintas en este host, con atascos en\nN=16 de 11,22s, 12,44s, 21,22s, 31,14s y 5,14s. En todas las ejecuciones asíncronas\nlimpias: ninguno.</p></div></div>\n<p>Lee primero las filas asíncronas. En N=8 la mediana del gateway es 0,90s y la del control\n0,85s. El callback, con todas sus idas y vueltas a Presidio, desaparece dentro de la\nlatencia del propio modelo.</p>\n<p>Ahora la fila bloqueante en N=8. Mediana de 0,96s contra un control de 0,90s. Eso es\n0,06s. Si midieras medianas y desplegaras, dirías que está bien — y el peor lote de esas\nmismas cinco tardó 1,70s mientras su propio control tardó 0,90s.</p>\n<p>En N=16 deja de esconderse. La mediana sube a 2,88s contra 1,70s, y la cola se sale del\ngráfico.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Sabes distinguir si un gateway lento es tu propio código o el modelo al que llama —\nejecuta la misma carga contra los dos, intercalada, y lee la cola en vez de la mediana.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"no-solo-se-vuelve-lento-descarta-peticiones\">No solo se vuelve lento. Descarta peticiones.<a href=\"https://development-wec.wiline.com/docs/es/tutorials/load-test-llm-gateway-blocking-callback/#no-solo-se-vuelve-lento-descarta-peticiones\" class=\"hash-link\" aria-label=\"Enlace directo al No solo se vuelve lento. Descarta peticiones.\" title=\"Enlace directo al No solo se vuelve lento. Descarta peticiones.\" translate=\"no\">​</a></h2>\n<p>Antes de añadir el conteo de fallos al script de carga, una ejecución bloqueante murió del\ntodo:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">httpx.RemoteProtocolError: Server disconnected without sending a response.</span><br></div></code></pre></div></div>\n<p>Dos de diez lotes de esa ejecución perdieron peticiones así. Cero lotes asíncronos lo\nhicieron nunca. El gateway no estuvo lento para esos clientes. Les colgó.</p>\n<p>Pregúntale al gateway qué cree que pasó:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> logs llm-gateway </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"httpcore/_sync\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> logs llm-gateway </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-A1</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"httpx.ReadTimeout\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-4</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">77</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">INFO:     172.22.0.1:47566 - \"POST /v1/chat/completions HTTP/1.1\" 200 OK</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">--</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">httpx.ReadTimeout: timed out</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">INFO:     172.22.0.1:52870 - \"POST /v1/chat/completions HTTP/1.1\" 200 OK</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Setenta y siete marcos de pila sync en el registro, y un ReadTimeout entre dos respuestas 200 OK\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw5-sync-timeout-d454e62c9e5512f71923e0ec2c0efbec.png\" width=\"645\" height=\"142\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figura 9.</strong> <code>httpcore/_sync</code> aparece 77 veces. El cliente asíncrono produciría <code>_async</code>.\nY el timeout está entre dos líneas <code>200 OK</code>.</p>\n<p>Ese <code>_sync</code> es la huella. Prueba que la llamada que falla es la llamada bloqueante a\nPresidio y no otra cosa de la pila. El error completo nombra a quien llama:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">LiteLLM:ERROR: logging_worker.py:103 - LoggingWorker error: timed out</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  File \".../httpcore/_sync/connection_pool.py\", line 236, in handle_request</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  httpcore.ReadTimeout: timed out</span><br></div></code></pre></div></div>\n<p><strong>Ahora mira lo que lo rodea.</strong> <code>200 OK</code>. Las peticiones tuvieron éxito. Lo que falló fue\nel enmascarado.</p>\n<p>Esa es la parte en la que vale la pena detenerse, porque toda la promesa de la Parte 4 era\nque la PII llega a tus rastros ya enmascarada. Bajo carga, con el cliente bloqueante, la\nllamada de enmascarado expira contra su propio límite de diez segundos mientras quien\nllama recibe un éxito normal. Nada en la respuesta te dice que la garantía dejó de\ncumplirse. Tendrías que estar leyendo el stderr del gateway para saberlo.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-está-pasando-en-realidad\">Qué está pasando en realidad<a href=\"https://development-wec.wiline.com/docs/es/tutorials/load-test-llm-gateway-blocking-callback/#qu%C3%A9-est%C3%A1-pasando-en-realidad\" class=\"hash-link\" aria-label=\"Enlace directo al Qué está pasando en realidad\" title=\"Enlace directo al Qué está pasando en realidad\" translate=\"no\">​</a></h2>\n<p><code>httpx.Client</code> dentro de un <code>async def</code> no cede el control. Cuando el callback llama a\nPresidio, el hilo que ejecuta el bucle de eventos se queda en una lectura de socket hasta\nque Presidio responde, y durante ese tiempo el bucle no atiende nada — ni la llamada al\nmodelo de otra petición, ni una respuesta que vuelve. Es un solo bucle, como confirmaste\nal principio.</p>\n<p>Cada petición dispara varias de estas llamadas: los mensajes, la copia en el objeto de\nregistro estándar, y la respuesta. Así que bajo concurrencia las peticiones no se\nsolapan. Se ponen en fila, y la espera de cada una es la suma del tiempo de Presidio de\ntodas las anteriores. Por eso el efecto no es un impuesto constante — depende de cuántas\npeticiones lleguen mientras el bucle está retenido, que es también la razón por la que los\nnúmeros son irregulares en vez de uniformemente peores.</p>\n<p>Pasada cierta profundidad de cola, las esperas superan el propio <code>timeout=10.0</code> del\nscrubber y el enmascarado se rinde, mientras las conexiones de cliente que esperan sobre\nun bucle congelado se caen. La mediana lo esconde todo porque a la mayoría de los lotes\nles toca suerte. La cola es la verdad.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"arreglarlo\">Arreglarlo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/load-test-llm-gateway-blocking-callback/#arreglarlo\" class=\"hash-link\" aria-label=\"Enlace directo al Arreglarlo\" title=\"Enlace directo al Arreglarlo\" translate=\"no\">​</a></h2>\n<p>El arreglo es el que adoptó la Parte 4, y esta es la evidencia a su favor:\n<code>httpx.AsyncClient</code> con <code>await</code>, que cede el bucle mientras Presidio trabaja.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">diff</span><span class=\"token plain\"> scrubber.py scrubber_blocking.py</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">26,27c26,27</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">&lt;     async with httpx.AsyncClient(timeout=10.0) as client:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">&lt;         r = await client.post(f\"{ANALYZER}/analyze\", json={\"text\": text, \"language\": \"en\"})</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">---</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">&gt;     with httpx.Client(timeout=10.0) as client:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">&gt;         r = client.post(f\"{ANALYZER}/analyze\", json={\"text\": text, \"language\": \"en\"})</span><br></div></code></pre></div></div>\n<p>Dos palabras y un <code>await</code>. Esa es toda la diferencia entre la Figura 6 y la Figura 7.</p>\n<p>Más workers no es el arreglo, y la documentación explica por qué mejor de lo que podríamos\nnosotros. Sobre <code>--timeout_worker_healthcheck</code>, describiendo <code>--num_workers &gt; 1</code>:</p>\n<blockquote>\n<p><em>\"the supervisor process pings each worker; a worker that does not respond within this\nwindow (for example because its event loop is blocked by synchronous work) is killed\nwith SIGKILL and replaced.\"</em></p>\n</blockquote>\n<p>Con un worker no hay supervisor, así que un bucle bloqueado se atasca. Con varios, un\nworker bloqueado es eliminado y todo lo que tenía en vuelo muere en vez de esperar.\nNinguna de las dos cosas arregla el trabajo sincrónico sobre un bucle de eventos. Arregla\nla llamada.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Prove it holds under load</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-no-probamos\">Lo que no probamos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/load-test-llm-gateway-blocking-callback/#lo-que-no-probamos\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que no probamos\" title=\"Enlace directo al Lo que no probamos\" translate=\"no\">​</a></h2>\n<p>Cinco repeticiones por celda bastan para mostrar que existe un atasco al lado de un\ncontrol estable. No bastan para caracterizar la distribución — con qué frecuencia, con qué\ngravedad, o cómo escala más allá de N=16. Si ejecutas esto en producción, ejecútalo más\ntiempo y mira los percentiles.</p>\n<p>No probamos <code>--num_workers &gt; 1</code>, respuestas en streaming, ni un Presidio lento o remoto en\nlugar de un contenedor local sano.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-el-timeout-te-cuesta-en-realidad\">Lo que el timeout te cuesta en realidad<a href=\"https://development-wec.wiline.com/docs/es/tutorials/load-test-llm-gateway-blocking-callback/#lo-que-el-timeout-te-cuesta-en-realidad\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que el timeout te cuesta en realidad\" title=\"Enlace directo al Lo que el timeout te cuesta en realidad\" translate=\"no\">​</a></h2>\n<p>La preocupación obvia, cuando la llamada de enmascarado expira en una petición que\ndevolvió <code>200 OK</code>, es que la carga sin enmascarar acabe igualmente en el rastro. No es\nasí.</p>\n<p>Carga sostenida — seis rondas de veinticuatro peticiones concurrentes, cada una con un\nnombre y un número de teléfono — produjo <strong>77 timeouts de enmascarado</strong> en el registro del\ngateway. De las 144 peticiones, <strong>62 rastros llegaron a Langfuse y 82 no llegaron\nnunca.</strong> Los 62 estaban correctamente enmascarados. Ninguno llevaba un nombre en claro.</p>\n<p>Así que el fallo no es un fallo de privacidad. Es un fallo de observabilidad, y silencioso:\nbajo carga sostenida más de la mitad del tráfico simplemente no se registra, mientras cada\ncliente recibe un éxito normal. Si estás leyendo el volumen de rastros como aproximación\ndel tráfico, o contando con los rastros para una pista de auditoría, esa brecha es lo que\nhay que vigilar — y es invisible desde el lado de la respuesta.</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>Mide la detección antes de medir cualquier otra cosa</div><div class=\"admonitionContent_BuS1\"><p>Construyendo esta prueba, las peticiones se etiquetaron con un marcador corto para poder\nencontrar cada rastro — <code>[TAG-07] Priya Raghunathan called from …</code>. Nueve rastros\nvolvieron entonces con el teléfono enmascarado y el nombre en claro, que se lee exactamente\ncomo una fuga inducida por la carga.</p><p>No lo era. De forma secuencial, sin carga alguna, esa frase enviada directamente a Presidio\ndevuelve solo <code>PHONE_NUMBER</code>; la misma frase sin el prefijo entre corchetes devuelve\n<code>PERSON</code> y <code>PHONE_NUMBER</code> a la vez. El marcador suprimió la detección del nombre, y el\ncallback enmascaró fielmente todo lo que Presidio informó.</p><p>Comprueba qué detecta tu analizador en tus cadenas exactas antes de concluir nada sobre el\nenmascarado bajo carga. Un instrumento que cambia lo que mide te va a entregar un hallazgo\nque no está ahí.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lecturas-adicionales\">Lecturas adicionales<a href=\"https://development-wec.wiline.com/docs/es/tutorials/load-test-llm-gateway-blocking-callback/#lecturas-adicionales\" class=\"hash-link\" aria-label=\"Enlace directo al Lecturas adicionales\" title=\"Enlace directo al Lecturas adicionales\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/\">Parte 4 — rastros limpios, respuestas intactas</a> — el callback medido aquí</li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/\">Parte 3 — enmascarar PII en el gateway</a> — la configuración de Presidio</li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/\">Observa producción con Langfuse</a> — donde aterrizan estos rastros</li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/load-test-llm-gateway-blocking-callback/",
            "title": "Prueba de carga en un gateway LLM: encuentra el atasco que la mediana esconde",
            "summary": "La Parte 4 midió una llamada bloqueante dentro de un callback en 200-350ms y advirtió que el daño real solo aparece bajo concurrencia. Esto lo ejecuta: dieciséis peticiones a la vez, con un control contra el upstream para distinguir la culpa de tu gateway de la del modelo. Las medianas de las dos versiones son casi idénticas. Una versión descarta peticiones y deja de enmascarar en silencio.",
            "date_modified": "2026-08-27T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "llm",
                "gateway",
                "litellm",
                "performance",
                "concurrency",
                "presidio",
                "observabilidad",
                "wec",
                "wec-inference"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/",
            "content_html": "<div class=\"tutorialHero\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"487\" height=\"249\" fill=\"none\" viewBox=\"0 0 487 249\" class=\"tutorialHero__brand\"><path fill=\"#1C3C3C\" fill-rule=\"evenodd\" d=\"M124.273.412h239.031c68.111 0 123.52 55.603 123.52 123.948s-55.409 123.948-123.52 123.948H124.273c-68.11 0-123.52-55.603-123.52-123.948S56.164.412 124.274.412M234.018 192.55c3.001 3.156 7.443 3 11.378 2.182l.039.02c1.827-1.486-.77-3.368-3.249-5.165-1.487-1.077-2.932-2.125-3.356-3.038 1.373-1.674-2.687-5.474-5.847-8.433-1.327-1.242-2.495-2.335-3.037-3.061-2.248-2.448-3.152-5.537-4.062-8.644-.603-2.061-1.208-4.131-2.211-6.027-6.176-14.339-13.248-28.561-23.165-40.699-6.373-8.067-13.646-15.291-20.922-22.518-4.691-4.66-9.382-9.319-13.835-14.206-4.581-4.73-7.338-10.556-10.1-16.392-2.312-4.886-4.627-9.779-8.018-14.04-10.268-15.196-42.686-19.346-47.44 2.124.019.662-.195 1.09-.78 1.52-2.63 1.928-4.968 4.11-6.935 6.76-4.812 6.721-5.553 18.118.448 24.158l.025-.388c.2-3.05.388-5.9 2.8-8.087 4.637 3.994 11.67 5.416 17.047 2.435 6.483 9.3 8.548 20.543 10.621 31.823 1.726 9.398 3.457 18.821 7.751 27.171l.266.443c2.524 4.202 5.089 8.472 8.326 12.142 1.176 1.823 3.59 3.791 6 5.755 3.18 2.591 6.353 5.177 6.663 7.416.015.974.01 1.961.006 2.954-.025 5.879-.051 11.967 3.716 16.801 2.084 4.228-3.02 8.475-7.131 7.949-2.254.312-4.716-.282-7.161-.871-3.346-.807-6.659-1.607-9.36-.064-.758.82-1.846.849-2.94.877-1.297.035-2.601.069-3.373 1.422-.158.402-.528.856-.913 1.328-.846 1.036-1.762 2.159-.665 3.016q.149-.111.294-.224c1.663-1.269 3.248-2.479 5.493-1.724-.299 1.658.772 2.103 1.843 2.547q.281.114.553.239c-.011.385-.087.774-.163 1.159-.18.921-.356 1.824.358 2.62.339-.344.639-.732.939-1.12.735-.95 1.474-1.904 2.802-2.25 2.92 3.9 5.862 2.28 9.554.248 4.164-2.293 9.281-5.111 16.396-1.125-2.727-.136-5.163.195-6.994 2.455-.448.507-.838 1.091-.039 1.754 4.21-2.728 5.961-1.748 7.61-.825 1.19.666 2.326 1.302 4.294.493.465-.242.93-.493 1.396-.745 3.161-1.705 6.366-3.434 10.118-2.839-2.803.808-3.8 2.584-4.888 4.524-.539.959-1.099 1.957-1.911 2.898-.429.429-.624.936-.137 1.656 5.869-.488 8.087-1.976 11.083-3.986 1.429-.959 3.036-2.038 5.302-3.183 2.505-1.542 5.009-.556 7.436.4 2.633 1.037 5.175 2.037 7.527-.264.743-.7 1.674-.708 2.602-.717a10 10 0 0 0 1.002-.043c-.732-3.92-4.861-3.874-9.052-3.827-4.847.054-9.778.109-9.632-5.972 4.504-3.076 4.546-8.415 4.585-13.461.01-1.218.019-2.419.091-3.567 3.313 1.847 6.817 3.291 10.299 4.725 3.276 1.35 6.533 2.692 9.593 4.354 3.195 5.143 8.182 11.962 14.826 11.514.175-.526.331-.974.526-1.5.383.067.787.169 1.199.273 1.743.442 3.611.915 4.509-1.15m130.213-58.45a20.54 20.54 0 0 0 14.504 5.994c5.44 0 10.658-2.156 14.505-5.994a20.44 20.44 0 0 0 6.007-14.469 20.44 20.44 0 0 0-6.007-14.469 20.54 20.54 0 0 0-21.882-4.624l-11.757-17.162-8.194 5.614 11.818 17.25a20.43 20.43 0 0 0-5.002 13.391 20.43 20.43 0 0 0 6.008 14.469m-36.808-55.576a20.55 20.55 0 0 0 21.408-1.964 20.46 20.46 0 0 0 7.34-10.483 20.4 20.4 0 0 0-.331-12.784 20.47 20.47 0 0 0-7.873-10.09 20.557 20.557 0 0 0-18.35-2.282 20.5 20.5 0 0 0-7.964 5.175 20.44 20.44 0 0 0-4.77 8.201 20.429 20.429 0 0 0 3.232 18.164 20.5 20.5 0 0 0 7.308 6.063m0 118.824a20.55 20.55 0 0 0 21.408-1.964 20.46 20.46 0 0 0 7.34-10.483 20.4 20.4 0 0 0-.331-12.783 20.47 20.47 0 0 0-7.873-10.092 20.55 20.55 0 0 0-26.314 2.894 20.45 20.45 0 0 0-4.77 8.201 20.43 20.43 0 0 0 3.232 18.164 20.5 20.5 0 0 0 7.308 6.063m18.857-72.629v-10.174h-31.394a20.3 20.3 0 0 0-4.398-8.342L322.3 88.704l-8.59-5.698-11.812 17.499a20.5 20.5 0 0 0-6.749-1.221 20.5 20.5 0 0 0-14.462 5.959 20.3 20.3 0 0 0-5.99 14.388 20.3 20.3 0 0 0 5.99 14.387 20.5 20.5 0 0 0 14.462 5.96 20.5 20.5 0 0 0 6.749-1.221l11.812 17.498 8.487-5.697-11.709-17.498a20.3 20.3 0 0 0 4.398-8.342z\" clip-rule=\"evenodd\"></path></svg><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/Postgresql_elephant.svg-62fedb3464ae364ae4e342c86b474ed6.png\" alt=\"PostgreSQL\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2255\" height=\"527\" fill=\"none\" viewBox=\"0 0 2255 527\" class=\"tutorialHero__langfuse\"><path fill=\"#1B1917\" d=\"M652.669 116.433c0-10.261-7.683-17.956-17.926-17.956H607V60h87.923v316.366h-42.254zM804.056 379.786c-39.693 0-72.131-27.362-72.131-68.831 0-41.042 28.596-69.686 84.935-69.686h39.693c7.256 0 12.805-5.558 12.805-12.826v-8.55c0-25.651-20.487-38.905-40.547-38.905-18.353 0-33.291 8.123-41.828 26.934h-45.668c11.95-44.462 46.095-65.41 88.349-65.41 40.547 0 82.374 22.231 82.374 77.808v156.046h-42.68v-14.109c0-5.13-5.549-7.695-9.817-4.702-16.646 11.97-32.438 22.231-55.485 22.231m5.975-38.477c18.78 0 34.572-9.406 52.498-27.362 4.695-4.702 6.829-10.688 6.829-17.1v-6.413c0-7.268-5.549-12.826-12.805-12.826H815.58c-27.743 0-40.974 12.398-40.974 31.637 0 17.956 12.377 32.064 35.425 32.064M961.855 376.366V147.642h42.255v19.666c0 5.13 6.4 6.84 10.24 2.992 13.23-12.825 31.59-27.788 60.61-27.788 36.71 0 70.85 23.086 70.85 75.671v158.183h-42.25V227.161c0-29.499-17.93-45.745-39.7-45.745-20.91 0-35 11.971-49.93 29.499-7.26 8.978-9.82 19.238-9.82 30.354v135.097zM1287.48 467c-52.5 0-85.79-24.796-95.61-63.273h46.1c7.25 15.818 20.06 25.651 45.67 25.651 34.14 0 55.48-20.093 55.48-65.838v-7.268c0-5.13-4.27-7.695-9.39-3.42-14.08 12.398-32.01 20.093-49.08 20.093-58.05 0-96.46-44.889-96.46-115.003 0-70.113 44.39-115.43 98.17-115.43 15.79 0 30.3 4.275 44.38 14.963 5.98 4.275 12.38.855 12.38-5.986v-3.847h42.26V363.54c0 72.678-43.97 103.46-93.9 103.46m-2.56-132.959c19.2 0 33.29-8.55 44.81-20.949 7.26-8.122 9.39-13.68 9.39-26.506v-61.563c0-12.826-2.13-20.948-10.67-29.071-9.39-8.978-22.62-14.964-39.69-14.964-35 0-61.89 29.072-61.89 76.954 0 47.883 24.76 76.099 58.05 76.099M1455.92 199.372c0-7.268-5.97-13.253-13.23-13.253h-32.44v-38.477h32.44c7.26 0 13.23-5.985 13.23-13.253v-5.986c0-45.744 23.48-68.403 69.15-68.403h29.02v38.477h-29.45c-17.5 0-26.46 9.833-26.46 29.926v5.986c0 7.268 5.97 13.253 13.23 13.253h42.68v38.477h-42.68c-7.26 0-13.23 5.985-13.23 13.253v176.994h-42.26zM1652.02 381.496c-35.85 0-69.14-23.086-69.14-75.671V147.642h42.25v150.06c0 29.499 17.07 44.889 37.13 44.889 21.77 0 35.85-11.97 50.79-29.499 7.26-8.977 9.82-19.238 9.82-30.354V147.642h42.25v228.724h-42.25V356.7c0-5.131-6.4-6.841-10.24-2.993-13.24 12.826-31.59 27.789-60.61 27.789M1893.57 381.496c-38.84 0-79.39-19.239-90.06-65.838h43.54c6.4 17.528 23.9 29.498 44.81 29.498 23.05 0 37.13-13.68 37.13-30.353 0-16.246-11.09-25.224-28.59-30.354l-36.28-10.261c-31.58-8.978-55.06-29.499-55.06-64.556 0-38.049 35.43-67.12 75.55-67.12 32.01 0 70.85 14.535 81.09 65.41h-40.55c-5.55-17.528-20.06-29.071-40.54-29.071-20.06 0-34.58 12.398-34.58 28.216 0 13.253 8.11 23.086 27.32 28.644l34.15 9.833c32.43 9.406 58.47 29.072 58.47 66.693 0 39.332-34.15 69.259-76.4 69.259M2098.54 381.496c-61.46 0-102.01-51.73-102.01-119.706s43.11-119.278 101.58-119.278c63.6 0 96.89 51.302 96.89 109.872v23.087h-144.26c-5.98 0-8.54 3.847-7.26 13.68 4.7 32.064 30.31 54.295 55.49 54.295 18.78 0 35-9.405 45.67-27.788h44.81c-16.22 40.187-49.94 65.838-90.91 65.838m43.11-141.51c6.83 0 9.39-3.42 7.68-14.108-4.69-26.506-24.33-45.317-51.22-45.317-25.6 0-47.37 18.811-54.2 45.745-2.56 9.833.85 13.68 6.83 13.68z\"></path><path fill=\"#FF5D5F\" d=\"m286.292 286.105 34.597 27.791s26.473-19.661 45.941-22.545c20.418-3.025 42.202 8.359 62.388 21.93 30.489 20.498 56.149 46.508 56.149 46.508l30.06-29.493s-82.879-89.795-148.597-81.672c-43.105 5.328-80.538 37.481-80.538 37.481\"></path><path fill=\"#4E9CFF\" d=\"M88.358 114.862 60 146.056s79.009 73.732 141.224 73.732c28.358 0 67.684-22.216 101.523-51.079 19.283-16.448 40.835-35.13 62.388-35.13 14.487 0 33.594 7.673 51.612 27.824 0 0 11.63-6.974 18.716-11.985 6.228-4.404 15.479-11.91 15.479-11.91-25.918-27.663-63.407-47.883-85.807-45.9-36.299.005-62.388 22.601-94.717 48.735s-45.94 36.907-69.194 36.907c-39.134 0-112.866-62.388-112.866-62.388M88.358 352.463 60 321.269s79.009-73.732 141.224-73.732c28.358 0 67.684 22.216 101.523 51.079 19.283 16.448 40.835 35.13 62.388 35.13 14.556 0 33.518-7.989 51.612-28.358 0 0 10.877 6.705 17.582 11.344 6.894 4.769 17.015 12.655 17.015 12.655-25.931 27.883-63.693 48.323-86.209 46.33-36.299-.005-57.851-19.24-90.179-45.374-32.329-26.133-50.478-40.268-73.732-40.268-39.134 0-112.866 62.388-112.866 62.388M458.142 185.149c-7.378 5.1-19.283 12.478-19.283 12.478s6.806 14.746 6.806 34.597-6.239 36.866-6.239 36.866 10.688 6.675 17.582 11.343c7.162 4.849 18.149 13.045 18.149 13.045s13.045-27.224 13.045-61.254-13.045-59.552-13.045-59.552-10.236 7.792-17.015 12.477\"></path><path fill=\"#FF5D5F\" d=\"m287.995 180.612 32.895-27.224s26.473 19.046 45.941 21.93c20.417 3.026 42.202-8.359 62.388-21.93 30.489-20.498 56.149-46.507 56.149-46.507l30.06 29.492s-82.879 89.795-148.597 81.672c-43.105-5.328-78.836-37.433-78.836-37.433M208.601 91c42.538 0 78.264 36.299 78.264 36.299s-9.941 7.832-16.448 13.045c-6.777 5.429-17.582 14.179-17.582 14.179s-18.711-19.851-44.234-19.851c-10.465 0-24.066 6.286-38.567 18.716-11.188 9.591-22.829 21.514-30.627 36.299-6.743 12.784-10.42 27.85-10.776 43.672-.446 19.873 6.597 40.704 18.149 57.283 7.743 11.112 16.983 19.474 26.657 26.657 12.555 9.322 25.648 15.881 35.164 15.881 10.166 0 19.306-3.533 26.09-6.806 10.776-6.239 19.278-13.612 19.278-13.612l33.463 27.791s-13.612 13.612-32.323 23.821c-12.091 5.963-27.632 11.91-46.508 11.91-18.862 0-40.767-10.022-61.254-25.522-13.244-10.021-26.225-21.895-36.298-36.299-16.51-23.607-25.017-52.328-24.96-81.104.057-29.136 9.451-57.993 26.094-81.672C138.273 117.657 176.86 91 208.601 91\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 4 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->4</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->4<!-- --> earned</span></div><div class=\"skillTracker__series\">Agent orchestration with LangGraph</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">State that survives a restart</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">2</span><span class=\"skillTracker__skill\" data-state=\"current\">Hand work between agents</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Governed tools an agent can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Engineer the context, not the prompt</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>Tienes dos agentes. Uno reserva citas. Otro gestiona facturación.</p>\n<p>Llega un ticket que necesita los dos: <em>cambia la fecha de mi instalación, y mi factura\nparece incorrecta.</em> Así que lo envías a los dos.</p>\n<p>El primero reserva el martes. El segundo ve que la cuenta está en mora y la congela. Los\ndos terminan casi en el mismo momento, y los dos guardan lo que decidieron.</p>\n<p>Solo se guarda uno de ellos. ¿Cuál? El que terminó primero — que depende de lo lenta que\nestuviera una llamada de API ese día. Así que reservas una cita en una cuenta congelada, o\ncongelas una cuenta a la que acabas de prometerle un ingeniero. Después parece que se tomó\nuna única decisión limpia.</p>\n<p>La <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/\">Parte 1</a> construyó un agente que\nejecuta sus pasos en un orden fijo. Este post tiene varios: un supervisor que elige quién\ntrabaja en qué, un agente que pasa el trabajo a otro a mitad de camino, y dos agentes\npuestos en el camino del otro a propósito — para descubrir qué hace LangGraph cuando no\nestán de acuerdo.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>Versiones verificadas: Python 3.10.12 · <code>langgraph</code> 1.2.11 ·\n<code>langgraph-checkpoint</code> 4.2.0 · <code>langgraph-checkpoint-postgres</code> 3.1.2 ·\n<code>psycopg</code> 3.3.4 · <code>langchain-openai</code> 1.6.0 · <code>langfuse</code> 4.14.5 contra el servidor\nLangfuse v3.205.1 OSS · imagen <code>postgres:17</code>. El contenedor <code>lg-checkpoints</code> y el\nvirtualenv de la Parte 1 se reutilizan sin cambios.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">Una instancia WEC — <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/cloud_portal/platform/compute/instances/compute_instance/\">despliega una</a> si aún no lo has hecho</li>\n<li class=\"\">Una clave de la API de Inferencia de WEC — <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/cloud_portal/platform/inference/api_keys/\">créala</a> en el portal</li>\n<li class=\"\">El contenedor de Postgres y el virtualenv de la <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/\">Parte 1</a></li>\n<li class=\"\">Una instancia de Langfuse para la última sección — mira <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/\">Observa producción con Langfuse</a></li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"alguien-tiene-que-decidir-quién-trabaja-en-esto\">Alguien tiene que decidir quién trabaja en esto<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/#alguien-tiene-que-decidir-qui%C3%A9n-trabaja-en-esto\" class=\"hash-link\" aria-label=\"Enlace directo al Alguien tiene que decidir quién trabaja en esto\" title=\"Enlace directo al Alguien tiene que decidir quién trabaja en esto\" translate=\"no\">​</a></h2>\n<p>Dos agentes significa que algo tiene que elegir entre ellos. Ese algo se llama supervisor, y\nes simplemente otro agente cuyo único trabajo es elegir al siguiente.</p>\n<p>La Parte 1 conectó sus pasos de antemano con <code>add_edge</code> — paso uno, luego dos, luego tres.\nUn supervisor no puede hacer eso, porque no sabe adónde va el trabajo hasta que lo lee.</p>\n<p>En su lugar el nodo devuelve un <code>Command</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">types </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Command</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">supervisor</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">Literal</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    nxt </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"invoice\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"ticket\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">lower</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"supervisor routing to </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">nxt</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">goto</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">nxt</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> update</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"supervisor-&gt;</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">nxt</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p><code>Command</code> hace dos trabajos a la vez: <code>goto</code> nombra el siguiente nodo, y <code>update</code> lleva un\ncambio de estado consigo. <strong>No hay ningún <code>add_edge</code> desde <code>supervisor</code> a nada</strong> — el\nenrutamiento ocurre en tiempo de ejecución, desde dentro de la función.</p>\n<p>Guarda todo como <code>lg_super.py</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">lg_super.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> sys</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> operator </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> add</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> typing </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Annotated</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> Literal</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> typing_extensions </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> TypedDict</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">graph </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> StateGraph</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> START</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> END</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">checkpoint</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">postgres </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> PostgresSaver</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">types </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Command</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">DB_URI </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"postgresql://langgraph:changeme@127.0.0.1:5434/checkpoints\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">THREAD </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">len</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"sup-1\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">TICKET </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">len</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"reschedule my install\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">TypedDict</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    ticket</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    completed</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Annotated</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> add</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">supervisor</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">Literal</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    nxt </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"invoice\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"ticket\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">lower</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"supervisor routing to </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">nxt</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">goto</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">nxt</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> update</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"supervisor-&gt;</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">nxt</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">scheduler</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler running\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">billing</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"billing running\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> StateGraph</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"supervisor\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> supervisor</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> scheduler</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> billing</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">START</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"supervisor\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> END</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> END</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"--draw\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token builtin\">compile</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get_graph</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">draw_mermaid</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">raise</span><span class=\"token plain\"> SystemExit</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> PostgresSaver</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">from_conn_string</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">DB_URI</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> cp</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    cp</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">setup</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    graph </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token builtin\">compile</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">checkpointer</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">cp</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    config </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"configurable\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"thread_id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> THREAD</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"FINAL:\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> graph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">invoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"ticket\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> TICKET</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> config</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>La rama <code>--draw</code> imprime el grafo y sale antes de tocar Postgres, así que puedes inspeccionar\nla topología sin base de datos. Se gana su sitio en la sección siguiente.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_super.py sup-1 </span><span class=\"token string\" style=\"color:#e3116c\">\"reschedule my install\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor routing to scheduler</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">scheduler running</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FINAL: {'ticket': 'reschedule my install', 'completed': ['supervisor-&gt;scheduler', 'scheduler']}</span><br></div></code></pre></div></div>\n<p>Y la otra rama, para mostrar que el enrutamiento depende de los datos y no está fijo en el\ncódigo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_super.py sup-2 </span><span class=\"token string\" style=\"color:#e3116c\">\"please fix the invoice on my account\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor routing to billing</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">billing running</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FINAL: {'ticket': 'please fix the invoice on my account', 'completed': ['supervisor-&gt;billing', 'billing']}</span><br></div></code></pre></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-anotación-que-no-es-una-pista-de-tipos\">La anotación que no es una pista de tipos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/#la-anotaci%C3%B3n-que-no-es-una-pista-de-tipos\" class=\"hash-link\" aria-label=\"Enlace directo al La anotación que no es una pista de tipos\" title=\"Enlace directo al La anotación que no es una pista de tipos\" translate=\"no\">​</a></h3>\n<p>Esa anotación de retorno parece typing normal que podrías quitar:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">supervisor</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">Literal</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><br></div></code></pre></div></div>\n<p>Quítala y el código se ejecuta igual — mismo enrutamiento, misma salida. Así que es\nopcional. Excepto que no lo es.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sed</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'s/ -&gt; Command\\[Literal\\[\"scheduler\", \"billing\"\\]\\]//'</span><span class=\"token plain\"> ~/lg_super.py </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> ~/lg_super_noann.py</span><br></div></code></pre></div></div>\n<p>Pídele a las dos versiones que se dibujen:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_super.py </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--draw</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_super_noann.py </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--draw</span><br></div></code></pre></div></div>\n<p>Con la anotación, las dos rutas posibles están ahí como aristas condicionales punteadas:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output (with annotation)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">__start__ --&gt; supervisor;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor -.-&gt; billing;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor -.-&gt; scheduler;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">billing --&gt; __end__;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">scheduler --&gt; __end__;</span><br></div></code></pre></div></div>\n<p>Sin ella, desaparecen, y el grafo afirma algo falso:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output (without annotation)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">__start__ --&gt; supervisor;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor --&gt; __end__;</span><br></div></code></pre></div></div>\n<p><code>scheduler</code> y <code>billing</code> siguen declarados como nodos, y ahora nada llega a ellos. El\ndiagrama dice que el supervisor va directo al final.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Dos grafos mermaid uno al lado del otro, uno con aristas condicionales punteadas a ambos agentes y otro donde el supervisor conecta directo a end\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg2-graphs-522fc91da9a7554bb7444d68736b3ee7.png\" width=\"585\" height=\"681\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figura 1.</strong> El mismo código, con una anotación de diferencia. El grafo de abajo está mal.</p>\n<p>La razón es mecánica: <code>goto</code> se decide dentro del cuerpo de la función, donde nada puede\ninspeccionarlo. La anotación es la <strong>única</strong> declaración de dónde tiene permitido un\nsupervisor mandar trabajo. Omítela y tu código sigue funcionando mientras cada diagrama, y\ncualquier otra cosa que lea la topología, miente en silencio.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span><code>draw_ascii()</code> necesita un paquete que LangGraph no incluye</div><div class=\"admonitionContent_BuS1\"><p><code>get_graph().draw_ascii()</code> lanza\n<code>ImportError: Install grandalf to draw graphs: 'pip install grandalf'</code>.\n<code>draw_mermaid()</code> no tiene dependencias extra y es lo que usan los ejemplos de arriba.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"traspasar-sin-preguntarle-al-supervisor\">Traspasar sin preguntarle al supervisor<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/#traspasar-sin-preguntarle-al-supervisor\" class=\"hash-link\" aria-label=\"Enlace directo al Traspasar sin preguntarle al supervisor\" title=\"Enlace directo al Traspasar sin preguntarle al supervisor\" translate=\"no\">​</a></h2>\n<p>Un supervisor decide una vez, al principio. Eso es un problema cuando el descubrimiento\nocurre más tarde.</p>\n<p>Toma un ticket que pide cambiar la fecha de una instalación, en una cuenta que resulta estar\nen mora. El supervisor ve una petición de agenda y enruta en consecuencia. El agente de\nagenda empieza a trabajar, lee la cuenta, y encuentra un problema que le pertenece a\nfacturación.</p>\n<p>El agente de agenda también puede enrutar — es un nodo, y los nodos devuelven <code>Command</code>.</p>\n<p>Copia <code>lg_super.py</code> a <code>lg_handoff.py</code>, reemplaza <code>scheduler</code> con la versión de abajo, y borra\nla línea <code>builder.add_edge(\"scheduler\", END)</code> — el nodo decide su propia salida ahora:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">lg_handoff.py (changed parts)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">scheduler</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">Literal</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"__end__\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"past due\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"ticket\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">lower</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler: account is past due, handing off to billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">goto</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> update</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler(handoff)\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler: booking it\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">goto</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">END</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> update</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler(booked)\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_handoff.py handoff-3 </span><span class=\"token string\" style=\"color:#e3116c\">\"reschedule my install, account is past due\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_handoff.py handoff-4 </span><span class=\"token string\" style=\"color:#e3116c\">\"reschedule my install\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor -&gt; scheduler</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">scheduler: account is past due, handing off to billing</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">billing running</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FINAL: {'ticket': '...past due', 'completed': ['supervisor', 'scheduler(handoff)', 'billing']}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor -&gt; scheduler</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">scheduler: booking it</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FINAL: {'ticket': 'reschedule my install', 'completed': ['supervisor', 'scheduler(booked)']}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Las dos ramas del traspaso — una enrutando a facturación, otra terminando en el agente de agenda\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg2-handoff-4bb2edcb710dc048a8d846ee980848c2.png\" width=\"912\" height=\"193\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figura 2.</strong> El supervisor nunca supo que facturación estaría implicada.</p>\n<p>Dos detalles que vale la pena guardar. <code>goto=END</code> funciona desde dentro de un <code>Command</code>, así\nque un nodo puede terminar la ejecución por sí mismo. Y <code>\"__end__\"</code> es la forma en cadena de\n<code>END</code> en ese <code>Literal</code> — la anotación necesita el valor literal, no la constante.</p>\n<p>Esta es la diferencia entre los dos patrones publicados. Un <strong>supervisor</strong> decide desde\nfuera y necesita conocer cada precondición por adelantado. Un <strong>traspaso</strong> deja que el\nagente que hace el trabajo redirija en cuanto aprende algo. La mayoría de los sistemas\nreales quieren los dos, que es lo que tiene este grafo.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"enviar-trabajo-a-dos-agentes-a-la-vez\">Enviar trabajo a dos agentes a la vez<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/#enviar-trabajo-a-dos-agentes-a-la-vez\" class=\"hash-link\" aria-label=\"Enlace directo al Enviar trabajo a dos agentes a la vez\" title=\"Enlace directo al Enviar trabajo a dos agentes a la vez\" translate=\"no\">​</a></h2>\n<p>Hasta aquí el supervisor elige un agente. A veces quieres los dos — revisar el calendario y\nla cuenta al mismo tiempo, en vez de esperar a uno antes de empezar el otro. <code>goto</code> acepta\nuna lista precisamente para eso:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">goto</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> update</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"supervisor\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Los dos se ejecutan en el mismo super-paso. Que es donde se pone interesante, porque ahora\npueden no estar de acuerdo.</p>\n<p>Copia <code>lg_super.py</code> a <code>lg_conflict.py</code>. Dale al estado dos tipos de campo:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">lg_conflict.py (changed parts)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">TypedDict</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    completed</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Annotated</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> add</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\">   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># has a reducer</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    decision</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token plain\">                          </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># no reducer</span><br></div></code></pre></div></div>\n<p>Y haz que cada agente escriba un valor distinto en el que no tiene reducer:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">scheduler</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"decision\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"book tuesday\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">billing</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"decision\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"refund first\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_conflict.py conflict-2</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor sending work to BOTH</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">billing deciding</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">scheduler deciding</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Traceback (most recent call last):</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ...</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  File \".../langgraph/pregel/_loop.py\", line 692, in after_tick</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    self.updated_channels = apply_writes(</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  File \".../langgraph/channels/last_value.py\", line 64, in update</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    raise InvalidUpdateError(msg)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">langgraph.errors.InvalidUpdateError: At key 'decision': Can receive only one value</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">per step. Use an Annotated key to handle multiple values.</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La traza del InvalidUpdateError terminando en last_value.py con el mensaje &amp;#39;At key decision&amp;#39;\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg2-conflict-906aa9ed90d5c25e9021f93fef593bcf.png\" width=\"993\" height=\"328\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figura 3.</strong> Los dos agentes terminaron. El grafo se negó a fusionar sus respuestas.</p>\n<p>Lee la traza con atención, porque tres cosas de ella importan.</p>\n<p><strong>Los dos agentes se ejecutaron.</strong> <code>billing deciding</code> y <code>scheduler deciding</code> se imprimieron\nlos dos. El trabajo se completó; el fallo vino después, en <code>after_tick</code> → <code>apply_writes</code>.\nEsto no son dos agentes chocando en vuelo. Es el grafo negándose a reconciliarlos en el\nlímite del paso.</p>\n<p><strong><code>completed</code> estuvo bien.</strong> Los dos agentes le añadieron valores en el mismo paso y nada se\nquejó, porque <code>add</code> dice qué significan dos valores. Solo falló <code>decision</code>.</p>\n<p><strong>LangGraph no elige un ganador.</strong> Ni \"el último gana\", ni una elección silenciosa. Que es\nel comportamiento correcto — la alternativa es un agente de reservas que a veces reserva el\nmartes y a veces emite un reembolso según qué tarea terminó primero por casualidad.</p>\n<p>Fíjate también en el orden: <code>billing</code> se imprimió antes que <code>scheduler</code>, al revés de la\nlista de <code>goto</code>. Las ramas paralelas terminan en el orden en que terminen.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"declarar-la-política\">Declarar la política<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/#declarar-la-pol%C3%ADtica\" class=\"hash-link\" aria-label=\"Enlace directo al Declarar la política\" title=\"Enlace directo al Declarar la política\" translate=\"no\">​</a></h3>\n<p>El arreglo no es evitar el desacuerdo. Es decir qué significa un desacuerdo. Copia\n<code>lg_conflict.py</code> a <code>lg_resolve.py</code> y añade el reducer:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">lg_resolve.py (changed parts)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">PRIORITY </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"refund first\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"book tuesday\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">prefer_higher_priority</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">old</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> new</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"The policy: a billing hold outranks a scheduling decision.\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    winner </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token builtin\">max</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">old</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> new</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token keyword\" style=\"color:#00009f\">lambda</span><span class=\"token plain\"> v</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> PRIORITY</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">v</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"  reducer: '</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">old</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">' vs '</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">new</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">' -&gt; '</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">winner</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">'\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> winner</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">TypedDict</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    completed</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Annotated</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> add</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    decision</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Annotated</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> prefer_higher_priority</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_resolve.py resolve-2</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  reducer: '' vs '' -&gt; ''</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor sending work to BOTH</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">billing deciding</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">scheduler deciding</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  reducer: '' vs 'refund first' -&gt; 'refund first'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  reducer: 'refund first' vs 'book tuesday' -&gt; 'refund first'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FINAL: {'completed': ['supervisor', 'billing', 'scheduler'], 'decision': 'refund first'}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El reducer disparándose tres veces, plegando dos decisiones de agentes en un ganador\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg2-resolve-e9d09a07776b7e33a95a0f951453e628.png\" width=\"624\" height=\"158\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figura 4.</strong> El print dentro del reducer hace visible la fusión.</p>\n<p>Mismo grafo, mismo desacuerdo, ningún error. Y la traza muestra tres cosas que la\ndocumentación no deja claras:</p>\n<p><strong>El reducer se ejecuta por pares, plegando hacia la izquierda.</strong> No una vez con los dos\nvalores — dos veces, acumulando. Primero fusiona el estado existente con la respuesta de\nfacturación, luego fusiona ese resultado con la del agente de agenda.</p>\n<p><strong>Por tanto debe ser independiente del orden.</strong> Como las ramas terminan en orden arbitrario,\nun reducer del tipo \"toma el más nuevo\" produce respuestas distintas en ejecuciones\ndistintas. <code>max</code> por prioridad no.</p>\n<p><strong>Se dispara antes de que se ejecute nada.</strong> Ese primer <code>reducer: '' vs '' -&gt; ''</code> es el\nestado inicial siendo escrito, fusionando el valor por defecto con el <code>decision: \"\"</code> pasado a\n<code>invoke</code>. Tu reducer ve entrada vacía y tiene que tolerarla.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"caerse-a-mitad-de-un-paso-paralelo\">Caerse a mitad de un paso paralelo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/#caerse-a-mitad-de-un-paso-paralelo\" class=\"hash-link\" aria-label=\"Enlace directo al Caerse a mitad de un paso paralelo\" title=\"Enlace directo al Caerse a mitad de un paso paralelo\" translate=\"no\">​</a></h2>\n<p>La Parte 1 mostró una ejecución matada reanudando desde su último nodo completado. El trabajo\nparalelo plantea una pregunta más afilada: si una rama termina y su hermana muere, ¿se pierde\nel trabajo terminado?</p>\n<p>Copia <code>lg_conflict.py</code> a <code>lg_partial.py</code>. Quita el campo <code>decision</code>, añade\n<code>RESUME = \"--resume\" in sys.argv</code>, pasa <code>None if RESUME else {...}</code> a <code>invoke</code> como en la\nParte 1, y reemplaza los dos especialistas por un nodo instantáneo y otro lo bastante lento\npara matarlo:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">lg_partial.py (changed parts)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">fast</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"fast running — finishes immediately\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"fast\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">slow</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"slow running — 30s window, KILL ME HERE\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">sleep</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token number\" style=\"color:#36acaa\">30</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"slow\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_partial.py partial-2</span><br></div></code></pre></div></div>\n<p>Pulsa Ctrl+C durante la ventana. Hacen falta dos pulsaciones, y la ejecución termina con algo\nque parece mucho peor que un <code>KeyboardInterrupt</code>:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output (tail)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  File \".../langgraph/pregel/_runner.py\", line 613, in commit</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    self.put_writes()(task.id, task.writes)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ...</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">RuntimeError: cannot schedule new futures after shutdown</span><br></div></code></pre></div></div>\n<p>Esa es la escritura del checkpoint de <code>fast</code> siendo confirmada contra un executor que ya se\nha cerrado. Se lee como pérdida de datos.</p>\n<p>No lo es:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_partial.py partial-2 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--resume</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">BEFORE  values={'completed': ['supervisor', 'fast']}  next=('slow',)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">slow running — 30s window, KILL ME HERE</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FINAL: {'completed': ['supervisor', 'fast', 'slow']}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La ejecución reanudada mostrando fast recuperado de Postgres y solo slow pendiente\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg2-partial-13329c0354dca69472571333fd73fb02.png\" width=\"657\" height=\"100\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figura 5.</strong> <code>fast</code> recuperado, no se volvió a ejecutar, y <code>next</code> nombra solo la rama que\nmurió.</p>\n<p><code>fast</code> está en el estado recuperado, aparece exactamente una vez en el resultado final, y\n<code>next=('slow',)</code> nombra solo la rama que nunca terminó. La escritura ya había aterrizado en\n<code>checkpoint_writes</code> — la misma tabla que impidió que <code>step_one</code> se repitiera en la Parte 1\nahora protege a una hermana completada.</p>\n<p>Vale la pena documentarlo precisamente porque el error sugiere lo contrario. Un\n<code>RuntimeError</code> en una ruta de cierre es ruido; el estado es lo que hay que comprobar.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"un-modelo-como-enrutador\">Un modelo como enrutador<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/#un-modelo-como-enrutador\" class=\"hash-link\" aria-label=\"Enlace directo al Un modelo como enrutador\" title=\"Enlace directo al Un modelo como enrutador\" translate=\"no\">​</a></h2>\n<p>Cada decisión de enrutamiento hasta aquí ha sido <code>\"invoice\" in ticket</code> — coincidencia de\npalabras clave. Ese es el enfoque que medimos desmoronándose en\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/\">el enrutador por complejidad del gateway</a>, donde un\nplural basta para no encontrar una palabra clave.</p>\n<p>Así que deja que el modelo decida. Copia <code>lg_super.py</code> a <code>lg_traced.py</code> — mismo grafo, un\nnodo cambiado:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">lg_traced.py (changed parts)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">llm </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ChatOpenAI</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    model</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"gemma4\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    base_url</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"https://inference.wiline.com/v1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    api_key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"WEC_API_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    temperature</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">supervisor</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">Literal</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    r </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> llm</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">invoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:#e3116c\">\"Route this support ticket to exactly one team. \"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:#e3116c\">\"Answer with one word, either scheduler or billing, nothing else.\\n\\n\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Ticket: </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">state</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'ticket'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    raw </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">strip</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">lower</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    nxt </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> raw </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"supervisor: model said </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">raw</span><span class=\"token string-interpolation interpolation conversion-option punctuation\" style=\"color:#393A34\">!r</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> -&gt; </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">nxt</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">goto</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">nxt</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> update</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"supervisor-&gt;</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">nxt</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Las credenciales vienen del entorno, que el <code>.env</code> del gateway ya contiene:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> -a</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">.</span><span class=\"token plain\"> ~/llm-gateway/.env</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> +a</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> ~/.venv/bin/python ~/lg_traced.py traced-1</span><br></div></code></pre></div></div>\n<p>El ticket es deliberadamente ambiguo — <em>\"my bill looks wrong and I need a new install date\"</em>\nmenciona los dos:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor: model said 'billing' -&gt; billing</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">billing running</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FINAL: {'ticket': 'my bill looks wrong and I need a new install date', 'completed': ['supervisor-&gt;billing', 'billing']}</span><br></div></code></pre></div></div>\n<p>Una palabra, sin frase envolvente, y el parseo se mantuvo. Eligió facturación — defendible,\nya que una cuenta sin pagar bloquea la instalación, que es la misma conclusión a la que llegó\nantes el traspaso leyendo la cuenta.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-costó-esa-palabra\">Lo que costó esa palabra<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/#lo-que-cost%C3%B3-esa-palabra\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que costó esa palabra\" title=\"Enlace directo al Lo que costó esa palabra\" translate=\"no\">​</a></h3>\n<p>Pedirle a un modelo que elija no es gratis, y la factura no está donde la buscarías. Activar\nel trazado muestra a dónde fueron el tiempo y los tokens. Es una línea, como en la Parte 1 —\n<code>CallbackHandler()</code> lee sus credenciales del entorno y va en el mismo diccionario de\nconfiguración que el id del hilo:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langfuse</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">langchain </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> CallbackHandler</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">handler </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> CallbackHandler</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">config </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"configurable\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"thread_id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> THREAD</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"callbacks\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">handler</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Rastro de Langfuse mostrando los spans de supervisor y billing, la insignia de tokens, el Command serializado con goto, y billing recibiendo la actualización de estado del supervisor\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg2-trace-771785326bcdaf3d0132233cc7bdf9fc.png\" width=\"1903\" height=\"998\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figura 6.</strong> Tres cosas enmarcadas: lo que costó el enrutamiento, la decisión como dato, y\nla carga llegando.</p>\n<p>El árbol de spans es el grafo:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">LangGraph        8.66s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  supervisor     8.53s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    ChatOpenAI   8.49s   52 → 412 (Σ 464)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  billing        0.01s</span><br></div></code></pre></div></div>\n<p>Tres hallazgos están en ese cuadro.</p>\n<p><strong>Una respuesta de una palabra costó 412 tokens de salida.</strong> Confirmado contra la API en vez\nde leído en la pantalla:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> -a</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">.</span><span class=\"token plain\"> ~/llm-gateway/.env</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> +a</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-u</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$LANGFUSE_PUBLIC_KEY</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string variable\" style=\"color:#36acaa\">$LANGFUSE_SECRET_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$LANGFUSE_HOST</span><span class=\"token string\" style=\"color:#e3116c\">/api/public/observations?limit=8\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token string\" style=\"color:#e3116c\">'.data[] | select(.name==\"ChatOpenAI\") | {model, usage}'</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"model\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"gemma4\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"usage\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"unit\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"TOKENS\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"input\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">52</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"output\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">412</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"total\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">464</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>La respuesta visible tiene siete caracteres. La factura es de 412 tokens de salida. Lo que\nsea que el modelo generó antes de decidirse, lo pagaste — y una llamada de enrutamiento\nparece lo más barato del grafo.</p>\n<p><strong>El supervisor es el nodo caro, no los especialistas.</strong> <code>billing</code> tardó 6ms y ningún token.\nEl enrutador tardó 8,49s y 464. En un grafo multi-agente, las decisiones de orquestación son\nel centro de coste.</p>\n<p><strong>La decisión de enrutamiento queda registrada como dato.</strong> La salida del supervisor en el\nrastro es el <code>Command</code> serializado:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"graph\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token null keyword\" style=\"color:#00009f\">null</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"update\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"completed\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"supervisor-&gt;billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"resume\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token null keyword\" style=\"color:#00009f\">null</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"goto\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>Y la entrada de <code>billing</code> contiene <code>[\"supervisor-&gt;billing\"]</code> — el <code>update</code> de ese <code>Command</code>\nllegando como estado del agente que lo recibe. Traspaso, coste y carga en un solo cuadro.</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>Un rastro muestra el camino tomado, no el grafo</div><div class=\"admonitionContent_BuS1\"><p><code>scheduler</code> no aparece en ninguna parte de este rastro — ni como span omitido, ni como nodo\ndesactivado. El panel Graph dibuja <code>__start__ → supervisor → billing → __end__</code>. No puedes\nsaber desde un rastro qué alternativas existían, solo cuál se ejecutó, que es una segunda\nrazón por la que importa la anotación <code>Literal</code> que falta: el grafo dibujado es el único\nsitio donde tus alternativas están escritas.</p></div></div>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Hand work between agents</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-viene\">Lo que viene<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/#lo-que-viene\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que viene\" title=\"Enlace directo al Lo que viene\" translate=\"no\">​</a></h2>\n<p>Parte 3: las herramientas que un agente tiene permitido llamar. Todo lo anterior confía en\nque cada nodo haga solo lo que debe — el próximo post pone las herramientas detrás de un\ngateway con permisos por herramienta, para que las capacidades de un agente se concedan en\nvez de asumirse.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"resolución-de-problemas\">Resolución de problemas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/#resoluci%C3%B3n-de-problemas\" class=\"hash-link\" aria-label=\"Enlace directo al Resolución de problemas\" title=\"Enlace directo al Resolución de problemas\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"importerror-install-grandalf-to-draw-graphs\"><code>ImportError: Install grandalf to draw graphs</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/#importerror-install-grandalf-to-draw-graphs\" class=\"hash-link\" aria-label=\"Enlace directo al importerror-install-grandalf-to-draw-graphs\" title=\"Enlace directo al importerror-install-grandalf-to-draw-graphs\" translate=\"no\">​</a></h3>\n<p><code>draw_ascii()</code> necesita un paquete del que LangGraph no depende. Usa <code>draw_mermaid()</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-diagrama-del-grafo-muestra-al-supervisor-yendo-directo-a-__end__\">El diagrama del grafo muestra al supervisor yendo directo a <code>__end__</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/#el-diagrama-del-grafo-muestra-al-supervisor-yendo-directo-a-__end__\" class=\"hash-link\" aria-label=\"Enlace directo al el-diagrama-del-grafo-muestra-al-supervisor-yendo-directo-a-__end__\" title=\"Enlace directo al el-diagrama-del-grafo-muestra-al-supervisor-yendo-directo-a-__end__\" translate=\"no\">​</a></h3>\n<p>Al nodo de enrutamiento le falta su anotación de retorno <code>-&gt; Command[Literal[...]]</code>. El\ncódigo sigue funcionando; solo la topología está mal.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"invalidupdateerror-at-key-x-can-receive-only-one-value-per-step\"><code>InvalidUpdateError: At key 'x': Can receive only one value per step</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/#invalidupdateerror-at-key-x-can-receive-only-one-value-per-step\" class=\"hash-link\" aria-label=\"Enlace directo al invalidupdateerror-at-key-x-can-receive-only-one-value-per-step\" title=\"Enlace directo al invalidupdateerror-at-key-x-can-receive-only-one-value-per-step\" translate=\"no\">​</a></h3>\n<p>Dos nodos escribieron la misma clave en un super-paso y esa clave no tiene reducer. O le das\nuno con <code>Annotated[T, fn]</code>, o dejas de despachar los dos nodos en paralelo.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"runtimeerror-cannot-schedule-new-futures-after-shutdown-después-de-ctrlc\"><code>RuntimeError: cannot schedule new futures after shutdown</code> después de Ctrl+C<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/#runtimeerror-cannot-schedule-new-futures-after-shutdown-despu%C3%A9s-de-ctrlc\" class=\"hash-link\" aria-label=\"Enlace directo al runtimeerror-cannot-schedule-new-futures-after-shutdown-después-de-ctrlc\" title=\"Enlace directo al runtimeerror-cannot-schedule-new-futures-after-shutdown-después-de-ctrlc\" translate=\"no\">​</a></h3>\n<p>La escritura del checkpoint de una rama completada compitió con el cierre del executor.\nComprueba <code>graph.get_state(config).values</code> antes de asumir que se perdió algo — en las\npruebas la escritura había aterrizado todas las veces.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-reducer-produce-resultados-distintos-en-ejecuciones-distintas\">El reducer produce resultados distintos en ejecuciones distintas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/#el-reducer-produce-resultados-distintos-en-ejecuciones-distintas\" class=\"hash-link\" aria-label=\"Enlace directo al El reducer produce resultados distintos en ejecuciones distintas\" title=\"Enlace directo al El reducer produce resultados distintos en ejecuciones distintas\" translate=\"no\">​</a></h3>\n<p>No es independiente del orden. Las ramas paralelas terminan en orden arbitrario, así que un\nreducer que dependa de qué valor llega segundo es no determinista por construcción.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lecturas-adicionales\">Lecturas adicionales<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/#lecturas-adicionales\" class=\"hash-link\" aria-label=\"Enlace directo al Lecturas adicionales\" title=\"Enlace directo al Lecturas adicionales\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/\">Parte 1 — checkpoints de un agente LangGraph</a> — estado que sobrevive a una caída</li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/\">Enrutar por complejidad</a> — lo que el enrutamiento por palabras clave no ve, medido</li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/\">Trazado a nivel de componente para llamadas a herramientas de agentes</a> — encontrar qué paso falló</li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/",
            "title": "Pasar trabajo entre agentes LangGraph sin corromper el estado compartido",
            "summary": "Un supervisor enruta trabajo a especialistas, un agente lo traspasa a mitad de tarea, y dos agentes escriben la misma clave de estado en el mismo paso. Uno de esos casos lanza un error en vez de elegir un ganador — y la decisión de enrutamiento cuesta 412 tokens de salida para decir una palabra.",
            "date_modified": "2026-08-26T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "agentes",
                "langgraph",
                "multi-agent",
                "state",
                "postgres",
                "langfuse",
                "autoalojamiento",
                "wec",
                "wec-inference"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/",
            "content_html": "<div class=\"tutorialHero\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"487\" height=\"249\" fill=\"none\" viewBox=\"0 0 487 249\" class=\"tutorialHero__brand\"><path fill=\"#1C3C3C\" fill-rule=\"evenodd\" d=\"M124.273.412h239.031c68.111 0 123.52 55.603 123.52 123.948s-55.409 123.948-123.52 123.948H124.273c-68.11 0-123.52-55.603-123.52-123.948S56.164.412 124.274.412M234.018 192.55c3.001 3.156 7.443 3 11.378 2.182l.039.02c1.827-1.486-.77-3.368-3.249-5.165-1.487-1.077-2.932-2.125-3.356-3.038 1.373-1.674-2.687-5.474-5.847-8.433-1.327-1.242-2.495-2.335-3.037-3.061-2.248-2.448-3.152-5.537-4.062-8.644-.603-2.061-1.208-4.131-2.211-6.027-6.176-14.339-13.248-28.561-23.165-40.699-6.373-8.067-13.646-15.291-20.922-22.518-4.691-4.66-9.382-9.319-13.835-14.206-4.581-4.73-7.338-10.556-10.1-16.392-2.312-4.886-4.627-9.779-8.018-14.04-10.268-15.196-42.686-19.346-47.44 2.124.019.662-.195 1.09-.78 1.52-2.63 1.928-4.968 4.11-6.935 6.76-4.812 6.721-5.553 18.118.448 24.158l.025-.388c.2-3.05.388-5.9 2.8-8.087 4.637 3.994 11.67 5.416 17.047 2.435 6.483 9.3 8.548 20.543 10.621 31.823 1.726 9.398 3.457 18.821 7.751 27.171l.266.443c2.524 4.202 5.089 8.472 8.326 12.142 1.176 1.823 3.59 3.791 6 5.755 3.18 2.591 6.353 5.177 6.663 7.416.015.974.01 1.961.006 2.954-.025 5.879-.051 11.967 3.716 16.801 2.084 4.228-3.02 8.475-7.131 7.949-2.254.312-4.716-.282-7.161-.871-3.346-.807-6.659-1.607-9.36-.064-.758.82-1.846.849-2.94.877-1.297.035-2.601.069-3.373 1.422-.158.402-.528.856-.913 1.328-.846 1.036-1.762 2.159-.665 3.016q.149-.111.294-.224c1.663-1.269 3.248-2.479 5.493-1.724-.299 1.658.772 2.103 1.843 2.547q.281.114.553.239c-.011.385-.087.774-.163 1.159-.18.921-.356 1.824.358 2.62.339-.344.639-.732.939-1.12.735-.95 1.474-1.904 2.802-2.25 2.92 3.9 5.862 2.28 9.554.248 4.164-2.293 9.281-5.111 16.396-1.125-2.727-.136-5.163.195-6.994 2.455-.448.507-.838 1.091-.039 1.754 4.21-2.728 5.961-1.748 7.61-.825 1.19.666 2.326 1.302 4.294.493.465-.242.93-.493 1.396-.745 3.161-1.705 6.366-3.434 10.118-2.839-2.803.808-3.8 2.584-4.888 4.524-.539.959-1.099 1.957-1.911 2.898-.429.429-.624.936-.137 1.656 5.869-.488 8.087-1.976 11.083-3.986 1.429-.959 3.036-2.038 5.302-3.183 2.505-1.542 5.009-.556 7.436.4 2.633 1.037 5.175 2.037 7.527-.264.743-.7 1.674-.708 2.602-.717a10 10 0 0 0 1.002-.043c-.732-3.92-4.861-3.874-9.052-3.827-4.847.054-9.778.109-9.632-5.972 4.504-3.076 4.546-8.415 4.585-13.461.01-1.218.019-2.419.091-3.567 3.313 1.847 6.817 3.291 10.299 4.725 3.276 1.35 6.533 2.692 9.593 4.354 3.195 5.143 8.182 11.962 14.826 11.514.175-.526.331-.974.526-1.5.383.067.787.169 1.199.273 1.743.442 3.611.915 4.509-1.15m130.213-58.45a20.54 20.54 0 0 0 14.504 5.994c5.44 0 10.658-2.156 14.505-5.994a20.44 20.44 0 0 0 6.007-14.469 20.44 20.44 0 0 0-6.007-14.469 20.54 20.54 0 0 0-21.882-4.624l-11.757-17.162-8.194 5.614 11.818 17.25a20.43 20.43 0 0 0-5.002 13.391 20.43 20.43 0 0 0 6.008 14.469m-36.808-55.576a20.55 20.55 0 0 0 21.408-1.964 20.46 20.46 0 0 0 7.34-10.483 20.4 20.4 0 0 0-.331-12.784 20.47 20.47 0 0 0-7.873-10.09 20.557 20.557 0 0 0-18.35-2.282 20.5 20.5 0 0 0-7.964 5.175 20.44 20.44 0 0 0-4.77 8.201 20.429 20.429 0 0 0 3.232 18.164 20.5 20.5 0 0 0 7.308 6.063m0 118.824a20.55 20.55 0 0 0 21.408-1.964 20.46 20.46 0 0 0 7.34-10.483 20.4 20.4 0 0 0-.331-12.783 20.47 20.47 0 0 0-7.873-10.092 20.55 20.55 0 0 0-26.314 2.894 20.45 20.45 0 0 0-4.77 8.201 20.43 20.43 0 0 0 3.232 18.164 20.5 20.5 0 0 0 7.308 6.063m18.857-72.629v-10.174h-31.394a20.3 20.3 0 0 0-4.398-8.342L322.3 88.704l-8.59-5.698-11.812 17.499a20.5 20.5 0 0 0-6.749-1.221 20.5 20.5 0 0 0-14.462 5.959 20.3 20.3 0 0 0-5.99 14.388 20.3 20.3 0 0 0 5.99 14.387 20.5 20.5 0 0 0 14.462 5.96 20.5 20.5 0 0 0 6.749-1.221l11.812 17.498 8.487-5.697-11.709-17.498a20.3 20.3 0 0 0 4.398-8.342z\" clip-rule=\"evenodd\"></path></svg><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/Postgresql_elephant.svg-62fedb3464ae364ae4e342c86b474ed6.png\" alt=\"PostgreSQL\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2255\" height=\"527\" fill=\"none\" viewBox=\"0 0 2255 527\" class=\"tutorialHero__langfuse\"><path fill=\"#1B1917\" d=\"M652.669 116.433c0-10.261-7.683-17.956-17.926-17.956H607V60h87.923v316.366h-42.254zM804.056 379.786c-39.693 0-72.131-27.362-72.131-68.831 0-41.042 28.596-69.686 84.935-69.686h39.693c7.256 0 12.805-5.558 12.805-12.826v-8.55c0-25.651-20.487-38.905-40.547-38.905-18.353 0-33.291 8.123-41.828 26.934h-45.668c11.95-44.462 46.095-65.41 88.349-65.41 40.547 0 82.374 22.231 82.374 77.808v156.046h-42.68v-14.109c0-5.13-5.549-7.695-9.817-4.702-16.646 11.97-32.438 22.231-55.485 22.231m5.975-38.477c18.78 0 34.572-9.406 52.498-27.362 4.695-4.702 6.829-10.688 6.829-17.1v-6.413c0-7.268-5.549-12.826-12.805-12.826H815.58c-27.743 0-40.974 12.398-40.974 31.637 0 17.956 12.377 32.064 35.425 32.064M961.855 376.366V147.642h42.255v19.666c0 5.13 6.4 6.84 10.24 2.992 13.23-12.825 31.59-27.788 60.61-27.788 36.71 0 70.85 23.086 70.85 75.671v158.183h-42.25V227.161c0-29.499-17.93-45.745-39.7-45.745-20.91 0-35 11.971-49.93 29.499-7.26 8.978-9.82 19.238-9.82 30.354v135.097zM1287.48 467c-52.5 0-85.79-24.796-95.61-63.273h46.1c7.25 15.818 20.06 25.651 45.67 25.651 34.14 0 55.48-20.093 55.48-65.838v-7.268c0-5.13-4.27-7.695-9.39-3.42-14.08 12.398-32.01 20.093-49.08 20.093-58.05 0-96.46-44.889-96.46-115.003 0-70.113 44.39-115.43 98.17-115.43 15.79 0 30.3 4.275 44.38 14.963 5.98 4.275 12.38.855 12.38-5.986v-3.847h42.26V363.54c0 72.678-43.97 103.46-93.9 103.46m-2.56-132.959c19.2 0 33.29-8.55 44.81-20.949 7.26-8.122 9.39-13.68 9.39-26.506v-61.563c0-12.826-2.13-20.948-10.67-29.071-9.39-8.978-22.62-14.964-39.69-14.964-35 0-61.89 29.072-61.89 76.954 0 47.883 24.76 76.099 58.05 76.099M1455.92 199.372c0-7.268-5.97-13.253-13.23-13.253h-32.44v-38.477h32.44c7.26 0 13.23-5.985 13.23-13.253v-5.986c0-45.744 23.48-68.403 69.15-68.403h29.02v38.477h-29.45c-17.5 0-26.46 9.833-26.46 29.926v5.986c0 7.268 5.97 13.253 13.23 13.253h42.68v38.477h-42.68c-7.26 0-13.23 5.985-13.23 13.253v176.994h-42.26zM1652.02 381.496c-35.85 0-69.14-23.086-69.14-75.671V147.642h42.25v150.06c0 29.499 17.07 44.889 37.13 44.889 21.77 0 35.85-11.97 50.79-29.499 7.26-8.977 9.82-19.238 9.82-30.354V147.642h42.25v228.724h-42.25V356.7c0-5.131-6.4-6.841-10.24-2.993-13.24 12.826-31.59 27.789-60.61 27.789M1893.57 381.496c-38.84 0-79.39-19.239-90.06-65.838h43.54c6.4 17.528 23.9 29.498 44.81 29.498 23.05 0 37.13-13.68 37.13-30.353 0-16.246-11.09-25.224-28.59-30.354l-36.28-10.261c-31.58-8.978-55.06-29.499-55.06-64.556 0-38.049 35.43-67.12 75.55-67.12 32.01 0 70.85 14.535 81.09 65.41h-40.55c-5.55-17.528-20.06-29.071-40.54-29.071-20.06 0-34.58 12.398-34.58 28.216 0 13.253 8.11 23.086 27.32 28.644l34.15 9.833c32.43 9.406 58.47 29.072 58.47 66.693 0 39.332-34.15 69.259-76.4 69.259M2098.54 381.496c-61.46 0-102.01-51.73-102.01-119.706s43.11-119.278 101.58-119.278c63.6 0 96.89 51.302 96.89 109.872v23.087h-144.26c-5.98 0-8.54 3.847-7.26 13.68 4.7 32.064 30.31 54.295 55.49 54.295 18.78 0 35-9.405 45.67-27.788h44.81c-16.22 40.187-49.94 65.838-90.91 65.838m43.11-141.51c6.83 0 9.39-3.42 7.68-14.108-4.69-26.506-24.33-45.317-51.22-45.317-25.6 0-47.37 18.811-54.2 45.745-2.56 9.833.85 13.68 6.83 13.68z\"></path><path fill=\"#FF5D5F\" d=\"m286.292 286.105 34.597 27.791s26.473-19.661 45.941-22.545c20.418-3.025 42.202 8.359 62.388 21.93 30.489 20.498 56.149 46.508 56.149 46.508l30.06-29.493s-82.879-89.795-148.597-81.672c-43.105 5.328-80.538 37.481-80.538 37.481\"></path><path fill=\"#4E9CFF\" d=\"M88.358 114.862 60 146.056s79.009 73.732 141.224 73.732c28.358 0 67.684-22.216 101.523-51.079 19.283-16.448 40.835-35.13 62.388-35.13 14.487 0 33.594 7.673 51.612 27.824 0 0 11.63-6.974 18.716-11.985 6.228-4.404 15.479-11.91 15.479-11.91-25.918-27.663-63.407-47.883-85.807-45.9-36.299.005-62.388 22.601-94.717 48.735s-45.94 36.907-69.194 36.907c-39.134 0-112.866-62.388-112.866-62.388M88.358 352.463 60 321.269s79.009-73.732 141.224-73.732c28.358 0 67.684 22.216 101.523 51.079 19.283 16.448 40.835 35.13 62.388 35.13 14.556 0 33.518-7.989 51.612-28.358 0 0 10.877 6.705 17.582 11.344 6.894 4.769 17.015 12.655 17.015 12.655-25.931 27.883-63.693 48.323-86.209 46.33-36.299-.005-57.851-19.24-90.179-45.374-32.329-26.133-50.478-40.268-73.732-40.268-39.134 0-112.866 62.388-112.866 62.388M458.142 185.149c-7.378 5.1-19.283 12.478-19.283 12.478s6.806 14.746 6.806 34.597-6.239 36.866-6.239 36.866 10.688 6.675 17.582 11.343c7.162 4.849 18.149 13.045 18.149 13.045s13.045-27.224 13.045-61.254-13.045-59.552-13.045-59.552-10.236 7.792-17.015 12.477\"></path><path fill=\"#FF5D5F\" d=\"m287.995 180.612 32.895-27.224s26.473 19.046 45.941 21.93c20.417 3.026 42.202-8.359 62.388-21.93 30.489-20.498 56.149-46.507 56.149-46.507l30.06 29.492s-82.879 89.795-148.597 81.672c-43.105-5.328-78.836-37.433-78.836-37.433M208.601 91c42.538 0 78.264 36.299 78.264 36.299s-9.941 7.832-16.448 13.045c-6.777 5.429-17.582 14.179-17.582 14.179s-18.711-19.851-44.234-19.851c-10.465 0-24.066 6.286-38.567 18.716-11.188 9.591-22.829 21.514-30.627 36.299-6.743 12.784-10.42 27.85-10.776 43.672-.446 19.873 6.597 40.704 18.149 57.283 7.743 11.112 16.983 19.474 26.657 26.657 12.555 9.322 25.648 15.881 35.164 15.881 10.166 0 19.306-3.533 26.09-6.806 10.776-6.239 19.278-13.612 19.278-13.612l33.463 27.791s-13.612 13.612-32.323 23.821c-12.091 5.963-27.632 11.91-46.508 11.91-18.862 0-40.767-10.022-61.254-25.522-13.244-10.021-26.225-21.895-36.298-36.299-16.51-23.607-25.017-52.328-24.96-81.104.057-29.136 9.451-57.993 26.094-81.672C138.273 117.657 176.86 91 208.601 91\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 4 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->4</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->4<!-- --> earned</span></div><div class=\"skillTracker__series\">Agent orchestration with LangGraph</div><ul class=\"skillTracker__steps\"><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">1</span><span class=\"skillTracker__skill\" data-state=\"current\">State that survives a restart</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-multi-agent-handoff-state/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Hand work between agents</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-governed-tools-agent-elicitation/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Governed tools an agent can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/scoped-mcp-tools-per-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Engineer the context, not the prompt</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>Tu agente lleva cuarenta segundos trabajando en la petición de un cliente. Ha leído el\nticket, ha consultado la cuenta y ha revisado los calendarios de tres ingenieros. Está a\npunto de reservar la cita.</p>\n<p>Entonces el proceso muere. Un despliegue que sale, el host que se queda sin memoria,\nalguien que reinicia el contenedor — da igual cuál de ellos.</p>\n<p>El agente no continúa donde lo dejó, porque no hay nada desde donde continuar. Todo lo que\naprendió vivía en variables dentro de un proceso que ya no existe. El cliente sigue\nesperando. Ejecútalo de nuevo y pagas todo ese trabajo por segunda vez. Y la parte que más\ndebería preocuparte: nadie puede decir si la cita se reservó en el último segundo antes de\nmorir.</p>\n<p>Un agente es un modelo dentro de un bucle. Como script de Python normal, ese bucle es\nexactamente igual de frágil que el proceso que lo contiene.</p>\n<p>Aquí construimos uno que guarda su estado en Postgres después de cada paso, para que una\ncaída no cueste nada.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>Versiones verificadas: Python 3.10.12 · <code>langgraph</code> 1.2.11 ·\n<code>langgraph-checkpoint</code> 4.2.0 · <code>langgraph-checkpoint-postgres</code> 3.1.2 ·\n<code>psycopg</code> 3.3.4 · <code>psycopg-pool</code> 3.3.1 · <code>langchain-openai</code> 1.6.0 ·\n<code>langfuse</code> 4.14.5 contra el servidor Langfuse v3.205.1 OSS · imagen <code>postgres:17</code>.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-problema-antes-de-la-solución\">El problema, antes de la solución<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/#el-problema-antes-de-la-soluci%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al El problema, antes de la solución\" title=\"Enlace directo al El problema, antes de la solución\" translate=\"no\">​</a></h2>\n<p>Lo que quieres es una ejecución <strong>reanudable</strong>: el resultado de cada paso escrito en algún\nsitio duradero en el momento en que termina, para que un proceso nuevo pueda continuar\ndesde ahí.</p>\n<p>Eso es lo que hace un checkpointer.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"por-qué-un-grafo-y-no-un-bucle\">Por qué un grafo y no un bucle<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/#por-qu%C3%A9-un-grafo-y-no-un-bucle\" class=\"hash-link\" aria-label=\"Enlace directo al Por qué un grafo y no un bucle\" title=\"Enlace directo al Por qué un grafo y no un bucle\" translate=\"no\">​</a></h3>\n<p>Un bucle con un <code>try/except</code> y unas cuantas escrituras a base de datos podría hacer esto\npara cuatro pasos fijos. Deja de funcionar en el momento en que el agente elige su propio\ncamino: cuando el modelo escoge la siguiente herramienta, \"¿dónde estamos?\" no tiene una\nrespuesta que puedas escribir.</p>\n<p>LangGraph te obliga a declarar el trabajo como un <strong>grafo</strong> — nodos con nombre, aristas\nexplícitas. Eso parece ceremonia hasta que quieres durabilidad.</p>\n<table><thead><tr><th>Un bucle</th><th>Un grafo</th></tr></thead><tbody><tr><td>Los pasos están implícitos en el flujo de control</td><td>Los pasos son nodos con nombre</td></tr><tr><td>\"¿Dónde estoy?\" no tiene respuesta</td><td>La posición es un valor que puedes leer</td></tr><tr><td>El estado vive en variables locales</td><td>El estado es un objeto declarado</td></tr><tr><td>No hay nada que guardar</td><td>Hay un momento obvio para guardar: los límites de nodo</td></tr></tbody></table>\n<p>El runtime sabe cuándo empieza y termina un nodo, así que tiene un momento obvio para\nguardar. El estado es un objeto declarado, así que hay algo concreto que escribir.</p>\n<p>Eso es lo que significa \"una máquina de estados determinista alrededor de un modelo no\ndeterminista\": la salida es impredecible, el siguiente paso no. Más sobre ese debate en\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/news/loop-engineering-graph-engineering-what-survives/\">\"Loop Engineering Is Dead\"</a>.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-vas-a-construir\">Lo que vas a construir<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/#lo-que-vas-a-construir\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que vas a construir\" title=\"Enlace directo al Lo que vas a construir\" translate=\"no\">​</a></h2>\n<!-- -->\n<p>Tres propiedades que los ejemplos habituales no tienen:</p>\n<ul>\n<li class=\"\"><strong>estado en Postgres</strong>, no en el proceso</li>\n<li class=\"\"><strong>una puerta de aprobación humana</strong> en la que el grafo puede quedarse días</li>\n<li class=\"\"><strong>cada nodo trazado</strong> como un span, con conteo de tokens y latencia por nodo</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">Una instancia WEC — <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/cloud_portal/platform/compute/instances/compute_instance/\">despliega una</a> si aún no lo has hecho</li>\n<li class=\"\">Una clave de la API de Inferencia de WEC — <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/cloud_portal/platform/inference/api_keys/\">créala</a> en el portal</li>\n<li class=\"\">Una instancia de Langfuse — mira <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/\">Observa producción con Langfuse</a></li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--instalar-y-por-qué-tres-paquetes\">Paso 1 — Instalar, y por qué tres paquetes<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/#paso-1--instalar-y-por-qu%C3%A9-tres-paquetes\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Instalar, y por qué tres paquetes\" title=\"Enlace directo al Paso 1 — Instalar, y por qué tres paquetes\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> venv .venv</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">source</span><span class=\"token plain\"> .venv/bin/activate</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">pip </span><span class=\"token function\" style=\"color:#d73a49\">install</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"langgraph\"</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"langgraph-checkpoint-postgres\"</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"psycopg[binary,pool]\"</span><br></div></code></pre></div></div>\n<table><thead><tr><th>Paquete</th><th>Qué hace</th><th>Por qué lo necesitas aquí</th></tr></thead><tbody><tr><td><code>langgraph</code></td><td>El runtime del grafo — nodos, aristas, estado</td><td>La orquestación en sí</td></tr><tr><td><code>langgraph-checkpoint-postgres</code></td><td>Escribe el estado en Postgres</td><td><strong>El objetivo de este tutorial</strong></td></tr><tr><td><code>psycopg[binary,pool]</code></td><td>Driver de Postgres + pool de conexiones</td><td><code>PostgresSaver</code> requiere un pool</td></tr></tbody></table>\n<p>Vale la pena detenerse en la fila del medio. <strong>La persistencia por defecto de LangGraph es\nen memoria.</strong> La durabilidad es opcional, en un paquete aparte que tienes que saber que\nhay que instalar. Esa es precisamente la razón por la que tantos ejemplos publicados\npierden el estado en silencio: nunca instalaron esto, y nada les avisó.</p>\n<p>Tres nombres arrastran más de tres paquetes:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">./.venv/bin/pip list </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-iE</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"langgraph|psycopg\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--version</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">langgraph                     1.2.11</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">langgraph-checkpoint          4.2.0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">langgraph-checkpoint-postgres 3.1.2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">langgraph-prebuilt            1.1.0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">langgraph-sdk                 0.4.3</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">psycopg                       3.3.4</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">psycopg-binary                3.3.4</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">psycopg-pool                  3.3.1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Python 3.10.12</span><br></div></code></pre></div></div>\n<p><code>langgraph-checkpoint</code> es el que hay que notar: es la interfaz abstracta, y <code>-postgres</code> es\nuna implementación de ella. Esa separación es la razón por la que cambiar Postgres por\nSQLite o Redis más adelante cambia una línea.</p>\n<p>El extra <code>pool</code> tampoco es decoración. Instala <code>psycopg</code> a secas y el fallo llega al\nconectar, no al instalar — que es un sitio mucho más confuso donde encontrarlo.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"pip list mostrando los paquetes langgraph y psycopg con sus versiones y Python 3.10.12\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg1-versions-21d23b4acd78bb5e757cf1a9f3949f97.png\" width=\"1580\" height=\"380\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figura 1.</strong> Ocho paquetes desde tres nombres — la interfaz de checkpoint y su\nimplementación en Postgres llegan por separado.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--una-base-de-datos-para-checkpoints-no-para-la-aplicación\">Paso 2 — Una base de datos para checkpoints, no para la aplicación<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/#paso-2--una-base-de-datos-para-checkpoints-no-para-la-aplicaci%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — Una base de datos para checkpoints, no para la aplicación\" title=\"Enlace directo al Paso 2 — Una base de datos para checkpoints, no para la aplicación\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--name</span><span class=\"token plain\"> lg-checkpoints </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-e</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">POSTGRES_USER</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">langgraph </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-e</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">POSTGRES_PASSWORD</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">changeme </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-e</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">POSTGRES_DB</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">checkpoints </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">127.0</span><span class=\"token plain\">.0.1:5434:5432 postgres:17</span><br></div></code></pre></div></div>\n<p>Tres decisiones deliberadas:</p>\n<p><strong>Una base de datos dedicada.</strong> Los checkpoints son intensivos en escritura — una fila por\ntransición de nodo, por hilo, para siempre. Mezclados en la base de datos de tu aplicación\nse convierten en una tabla que te da miedo truncar. Separados, son desechables.</p>\n<p><strong><code>127.0.0.1:5434:5432</code>, no <code>-p 5434:5432</code>.</strong> La forma corta escucha en todas las\ninterfaces, y Docker escribe sus propias reglas de iptables que se saltan UFW — así que un\nfirewall que parece correcto no está protegiendo este puerto. Lo cubrimos en\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/\">Endurecer redes Docker</a>.</p>\n<p>El número de la izquierda es el puerto del host, el de la derecha el del contenedor. En una\ninstancia vacía usa <code>5432:5432</code>. Si ya autoalojas algo con base de datos, ese puerto\nprobablemente esté ocupado — elige cualquiera libre.</p>\n<p><strong><code>postgres:17</code> fijado.</strong> <code>latest</code> significa que alguien que lea esto en seis meses\nejecutará algo que nunca probaste.</p>\n<p>Estar en marcha no es lo mismo que estar listo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> lg-checkpoints pg_isready </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-U</span><span class=\"token plain\"> langgraph </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> checkpoints</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">/var/run/postgresql:5432 - accepting connections</span><br></div></code></pre></div></div>\n<p>Un contenedor informa <code>Up</code> en el instante en que arranca, mientras Postgres dentro todavía\nse está inicializando. Conectar demasiado pronto da un error de conexión que parece un\nproblema de configuración y no lo es.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--el-grafo-deliberadamente-sin-modelo\">Paso 3 — El grafo, deliberadamente sin modelo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/#paso-3--el-grafo-deliberadamente-sin-modelo\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — El grafo, deliberadamente sin modelo\" title=\"Enlace directo al Paso 3 — El grafo, deliberadamente sin modelo\" translate=\"no\">​</a></h2>\n<p>Ningún LLM en este paso. Si algo falla ahora, es culpa del checkpointer y no del modelo — y\nseparar esas dos cosas es la mayor parte de lo que consiste depurar un agente.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-objeto-de-estado\">El objeto de estado<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/#el-objeto-de-estado\" class=\"hash-link\" aria-label=\"Enlace directo al El objeto de estado\" title=\"Enlace directo al El objeto de estado\" translate=\"no\">​</a></h3>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> operator </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> add</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> typing </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Annotated</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> typing_extensions </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> TypedDict</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">TypedDict</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    completed</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Annotated</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> add</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><br></div></code></pre></div></div>\n<p>Esto declara qué recuerda el grafo. <code>TypedDict</code> da la forma; la parte <code>Annotated[..., add]</code>\nes la mitad interesante.</p>\n<p>Por defecto, cuando un nodo devuelve un valor para una clave, ese valor <strong>reemplaza</strong> lo\nque había. <code>Annotated[list[str], add]</code> adjunta un <strong>reducer</strong> — una función que combina el\nvalor viejo con el nuevo en vez de sobrescribirlo. Con <code>operator.add</code> sobre una lista, eso\nsignifica añadir al final.</p>\n<table><thead><tr><th>Declaración</th><th><code>step_one</code> devuelve <code>[\"a\"]</code>, luego <code>step_two</code> devuelve <code>[\"b\"]</code></th></tr></thead><tbody><tr><td><code>completed: list[str]</code></td><td><code>[\"b\"]</code> — el primer resultado se pierde</td></tr><tr><td><code>completed: Annotated[list[str], add]</code></td><td><code>[\"a\", \"b\"]</code> — acumula</td></tr></tbody></table>\n<p>Equivócate en esto y tu estado olvida en silencio todo excepto el último nodo.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"los-nodos\">Los nodos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/#los-nodos\" class=\"hash-link\" aria-label=\"Enlace directo al Los nodos\" title=\"Enlace directo al Los nodos\" translate=\"no\">​</a></h3>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">step_one</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_one running\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">sleep</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"step_one\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">step_two</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_two running — 30s window, KILL ME HERE\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">sleep</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token number\" style=\"color:#36acaa\">30</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"step_two\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>Un nodo es una función normal. Recibe el estado actual y devuelve una <strong>actualización\nparcial</strong> — solo las claves que cambió, no el objeto entero. El runtime fusiona esa\nactualización usando los reducers que declaraste.</p>\n<p>El sleep de treinta segundos en <code>step_two</code> representa un paso lento — una llamada a un\nmodelo, una API externa, un trabajo por lotes. Es también lo que te da tiempo para matar el\nproceso en el momento justo en el paso siguiente.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"conectar-el-grafo\">Conectar el grafo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/#conectar-el-grafo\" class=\"hash-link\" aria-label=\"Enlace directo al Conectar el grafo\" title=\"Enlace directo al Conectar el grafo\" translate=\"no\">​</a></h3>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> StateGraph</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> name</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> fn </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_one\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> step_one</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_two\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> step_two</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_three\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> step_three</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> fn</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">START</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"step_one\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_one\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"step_two\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_two\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"step_three\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_three\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> END</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p><code>StateGraph(State)</code> vincula el esquema. <code>add_node</code> registra una función bajo un nombre — ese\nnombre es el que aparece en los checkpoints y en los rastros más adelante. <code>add_edge</code>\ndeclara el orden, con <code>START</code> y <code>END</code> como los centinelas que marcan la entrada y la salida.</p>\n<p>Este grafo es una línea recta. La ramificación llega en la parte 2.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"adjuntar-el-checkpointer\">Adjuntar el checkpointer<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/#adjuntar-el-checkpointer\" class=\"hash-link\" aria-label=\"Enlace directo al Adjuntar el checkpointer\" title=\"Enlace directo al Adjuntar el checkpointer\" translate=\"no\">​</a></h3>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> PostgresSaver</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">from_conn_string</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">DB_URI</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> checkpointer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    checkpointer</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">setup</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    graph </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token builtin\">compile</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">checkpointer</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">checkpointer</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    config </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"configurable\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"thread_id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> THREAD</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    result </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> graph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">invoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> config</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Cuatro líneas, cada una con su trampa.</p>\n<p><strong><code>with ... as checkpointer</code>.</strong> <code>from_conn_string</code> está decorado con <code>@contextmanager</code> en el\ncódigo fuente — <em>cede</em> un valor, no lo devuelve. Asignarlo directamente te entrega un objeto\ngestor de contexto donde esperabas un saver, y el error llega mucho más tarde que el fallo.</p>\n<p>También abre la conexión con <code>autocommit=True</code>, <code>prepare_threshold=0</code> y\n<code>row_factory=dict_row</code>. Si pasas tu propia conexión tienes que configurar eso tú, o\n<code>.setup()</code> puede parecer que funcionó y no guardar nada. Por qué hace falta cada uno no está\ndocumentado en ninguna parte — mira la\n<a href=\"https://github.com/langchain-ai/langgraph/issues/4937\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">issue #4937</a>, cerrada sin respuesta.</p>\n<p><strong><code>.setup()</code></strong> es obligatorio en el primer uso. Crea las tablas y ejecuta las migraciones.</p>\n<p><strong><code>compile(checkpointer=...)</code>.</strong> Sin este argumento el grafo se ejecuta perfectamente y no\nguarda nada. No hay ninguna advertencia. Es la razón más probable por la que alguien cree\nque los checkpoints \"no funcionan\".</p>\n<p><strong><code>thread_id</code>.</strong> El identificador de una conversación o de una ejecución. Cada checkpoint\nestá limitado a él, y reanudar significa pasar el mismo. Tiene que quedarse por debajo de\n255 caracteres.</p>\n<p>Todo junto, eso es <code>lg_agent.py</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">lg_agent.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> time</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> operator </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> add</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> typing </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Annotated</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> typing_extensions </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> TypedDict</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">graph </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> StateGraph</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> START</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> END</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">checkpoint</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">postgres </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> PostgresSaver</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">DB_URI </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"postgresql://langgraph:changeme@127.0.0.1:5434/checkpoints\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">THREAD </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">len</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"ticket-1\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">RESUME </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"--resume\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">TypedDict</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    completed</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Annotated</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> add</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">step_one</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_one running\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">sleep</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"step_one\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">step_two</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_two running — 30s window, KILL ME HERE\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">sleep</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token number\" style=\"color:#36acaa\">30</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"step_two\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">step_three</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_three running\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"step_three\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> StateGraph</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> name</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> fn </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_one\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> step_one</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_two\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> step_two</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_three\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> step_three</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> fn</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">START</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"step_one\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_one\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"step_two\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_two\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"step_three\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_three\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> END</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> PostgresSaver</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">from_conn_string</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">DB_URI</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> checkpointer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    checkpointer</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">setup</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    graph </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token builtin\">compile</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">checkpointer</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">checkpointer</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    config </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"configurable\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"thread_id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> THREAD</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    snap </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> graph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get_state</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">config</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"BEFORE  values=</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">snap</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">values</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">  next=</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">snap</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation builtin\">next</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    result </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> graph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">invoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> RESUME </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> config</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"FINAL:\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> result</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    snap </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> graph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get_state</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">config</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"AFTER   values=</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">snap</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">values</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">  next=</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">snap</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation builtin\">next</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p><code>RESUME</code> es lo que hace que <code>--resume</code> funcione: en una reanudación la entrada es <code>None</code> en\nvez de un estado nuevo, que es lo que cubre el paso siguiente.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">python lg_agent.py ticket-41</span><br></div></code></pre></div></div>\n<p>Después de la primera ejecución existen cuatro tablas:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> lg-checkpoints psql </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-U</span><span class=\"token plain\"> langgraph </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> checkpoints </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'\\dt'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> public | checkpoint_blobs      | table | langgraph</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> public | checkpoint_migrations | table | langgraph</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> public | checkpoint_writes     | table | langgraph</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> public | checkpoints           | table | langgraph</span><br></div></code></pre></div></div>\n<table><thead><tr><th>Tabla</th><th>Contiene</th></tr></thead><tbody><tr><td><code>checkpoints</code></td><td>Una fila por límite de nodo — el índice de instantáneas</td></tr><tr><td><code>checkpoint_blobs</code></td><td>Los valores de estado serializados</td></tr><tr><td><code>checkpoint_writes</code></td><td>Escrituras pendientes de nodos que terminaron en un super-paso</td></tr><tr><td><code>checkpoint_migrations</code></td><td>Registro de la versión del esquema</td></tr></tbody></table>\n<p><code>checkpoint_writes</code> es la que hace que funcione la recuperación tras una caída, como\nmuestra el paso siguiente.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Las cuatro tablas de checkpoint creadas por setup()\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg1-tables-f26befffa3adb092e0cecaa4126737c2.png\" width=\"1520\" height=\"380\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figura 2.</strong> <code>.setup()</code> escribe el esquema por ti — sin DDL manual.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"leer-la-posición\">Leer la posición<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/#leer-la-posici%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al Leer la posición\" title=\"Enlace directo al Leer la posición\" translate=\"no\">​</a></h3>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">snap </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> graph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get_state</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">config</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"BEFORE  values=</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">snap</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">values</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">  next=</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">snap</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation builtin\">next</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p><code>get_state</code> devuelve un <code>StateSnapshot</code>. Importan dos campos: <code>values</code> es el estado ahora\nmismo, y <code>next</code> es la tupla de nodos que quedan por ejecutar. <code>next=()</code> significa que el\nhilo terminó o nunca empezó; cualquier otra cosa nombra lo que está pendiente.</p>\n<p><code>next</code> es cómo respondes \"¿dónde está esta ejecución?\" — la pregunta que un bucle no puede\nresponder.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--mátalo-y-mira-cómo-vuelve\">Paso 4 — Mátalo, y mira cómo vuelve<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/#paso-4--m%C3%A1talo-y-mira-c%C3%B3mo-vuelve\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — Mátalo, y mira cómo vuelve\" title=\"Enlace directo al Paso 4 — Mátalo, y mira cómo vuelve\" translate=\"no\">​</a></h2>\n<p>Arranca un hilo nuevo y pulsa Ctrl+C durante la ventana de treinta segundos.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">python lg_agent.py ticket-42</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">BEFORE  values={}  next=()</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">step_one running</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">step_two running — 30s window, KILL ME HERE</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">^CKeyboardInterrupt</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"KeyboardInterrupt lanzado desde dentro de step_two\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg1-killed-13e4a54ded6e98bd000ca28160e0eb18.png\" width=\"1594\" height=\"858\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figura 3.</strong> Una caída real — la traza viene de dentro de <code>step_two</code>, que por tanto nunca\nse completó.</p>\n<p>Ahora reanuda el mismo <code>thread_id</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">python lg_agent.py ticket-42 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--resume</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">BEFORE  values={'completed': ['step_one']}  next=('step_two',)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">step_two running — 30s window, KILL ME HERE</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">step_three running</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FINAL: {'completed': ['step_one', 'step_two', 'step_three']}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">AFTER   values={'completed': [...]}  next=()</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La ejecución reanudada mostrando el estado recuperado y next=(&amp;#39;step_two&amp;#39;,)\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg1-resumed-060c0f536dc33b295570816403af58d0.png\" width=\"1220\" height=\"266\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figura 4.</strong> La línea <code>BEFORE</code> es la prueba — estado recuperado de Postgres por un proceso\ncompletamente nuevo.</p>\n<p>Lee esa primera línea con atención, porque es el tutorial entero:</p>\n<ul>\n<li class=\"\"><code>values</code> muestra el resultado de <code>step_one</code>, recuperado de Postgres por un proceso que no\nexistía cuando se produjo</li>\n<li class=\"\"><code>next=('step_two',)</code> — el grafo sabe exactamente qué nodo no terminó</li>\n<li class=\"\"><strong><code>step_one</code> no se ejecutó otra vez.</strong> Se ejecutó una sola vez a lo largo de tres\ninvocaciones</li>\n</ul>\n<p>Ese último punto es lo que compra <code>checkpoint_writes</code>: LangGraph conserva las escrituras\ncompletadas de un super-paso, así que reanudar no repite trabajo que ya tuvo éxito.</p>\n<p>La llamada de reanudación en sí es:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">graph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">invoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> config</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p><code>None</code> como entrada significa \"sin entrada nueva — continúa desde el checkpoint\". Vale la\npena saberlo: esto está documentado en la página de interrupciones para breakpoints\nestáticos, pero no en las páginas de persistencia o de ejecución duradera, que es donde\nmirarías cuando tu proceso acaba de morir. Funciona; simplemente no está escrito donde lo\nnecesitas.</p>\n<p>Mátalo una segunda vez y reanuda otra vez. Funciona, porque leer un checkpoint no lo\nconsume.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5--pausa-para-una-persona\">Paso 5 — Pausa para una persona<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/#paso-5--pausa-para-una-persona\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 5 — Pausa para una persona\" title=\"Enlace directo al Paso 5 — Pausa para una persona\" translate=\"no\">​</a></h2>\n<p>Una caída es una parada accidental. Una aprobación es una deliberada — y una vez que puedes\nsobrevivir a la primera, la segunda es casi gratis.</p>\n<p>El agente está a punto de escribir una cita en el calendario de un cliente. Eso es\nirreversible y lo ve una persona real, así que quieres que alguien lo revise antes.</p>\n<p>Sin estado duradero, esperar a una persona significa mantener un proceso abierto. Con el\nestado en Postgres el agente se detiene, el proceso sale, y la aprobación llega cuando\nllegue — diez minutos o el lunes por la mañana.</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">types </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> interrupt</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> Command</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">approval</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    decision </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> interrupt</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"question\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Approve step_two?\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"so_far\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"approval\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"approved\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> decision</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>Guárdalo como <code>lg_agent2.py</code> y ejecútalo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">python lg_agent2.py booking-7</span><br></div></code></pre></div></div>\n<p><code>interrupt()</code> hace dos cosas a la vez: detiene el grafo, y entrega su argumento a quien lo\nhaya llamado. Ese argumento es tu pregunta para la persona — cualquier valor serializable a\nJSON.</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">step_one running</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">RESULT: {'completed': ['step_one'], 'approved': '',</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">         '__interrupt__': [Interrupt(value={'question': 'Approve step_two?',</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">         'so_far': ['step_one']}, id='29f211cf5a73ea78e0f694e1bd38822b')]}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">AFTER   next=('approval',)  interrupts=(Interrupt(...),)</span><br></div></code></pre></div></div>\n<p>Ninguna excepción, código de salida 0. Nota la diferencia con el Paso 4: la caída producía\nuna traza, esto produce un retorno limpio.</p>\n<p>La pausa aparece en dos sitios, y la diferencia importa:</p>\n<table><thead><tr><th>Dónde</th><th>Uso</th></tr></thead><tbody><tr><td><code>__interrupt__</code> en el resultado de invoke</td><td>Quien acaba de ejecutar el grafo</td></tr><tr><td><code>snapshot.interrupts</code></td><td>Cualquier otro proceso — un panel que muestre la pregunta</td></tr></tbody></table>\n<p>El <code>id</code> correlaciona una respuesta con la pausa correcta, que importa en cuanto hay más de\nuna aprobación pendiente.</p>\n<p>El proceso ya no existe; el estado está en Postgres. Una hora o una semana después:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">python lg_agent2.py booking-7 </span><span class=\"token function\" style=\"color:#d73a49\">yes</span><br></div></code></pre></div></div>\n<p>que llama a:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">graph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">invoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">Command</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">resume</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"yes\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> config</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">approval resumed with: yes</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">step_two running (approved=yes)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">AFTER   next=()  interrupts=()</span><br></div></code></pre></div></div>\n<p><code>Command(resume=...)</code> es distinto de <code>invoke(None, ...)</code>. <code>None</code> dice \"continúa\";\n<code>Command(resume=X)</code> dice \"continúa, y <code>interrupt()</code> debería devolver <code>X</code>\". Ese valor vino de\nla línea de comandos, pasó por el retorno de <code>interrupt()</code>, entró en el estado, y lo leyó el\nnodo siguiente.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El grafo pausándose en la interrupción y reanudando con el valor de aprobación\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg1-interrupt-ea585f38dd6726bebaeeff4db224460a.png\" width=\"2214\" height=\"488\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figura 5.</strong> Pausa y reanudación — dos procesos distintos, una sola ejecución.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-parte-que-te-va-a-morder\">La parte que te va a morder<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/#la-parte-que-te-va-a-morder\" class=\"hash-link\" aria-label=\"Enlace directo al La parte que te va a morder\" title=\"Enlace directo al La parte que te va a morder\" translate=\"no\">​</a></h3>\n<p>Al reanudar, LangGraph vuelve a ejecutar el nodo interrumpido <strong>desde arriba</strong>. Esta vez\n<code>interrupt()</code> devuelve tu valor en vez de pausar — pero todo lo que está por encima en esa\nfunción ya se ejecutó una segunda vez.</p>\n<p>Pon un print antes de la interrupción, luego ejecuta un hilo nuevo y apruébalo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">python lg_agent2.py booking-8</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">python lg_agent2.py booking-8 approve</span><br></div></code></pre></div></div>\n<p>Aparece en las dos ejecuciones, para una sola aprobación:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">run 1:  step_one running</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        SIDE EFFECT — before interrupt</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        RESULT: {... '__interrupt__': [Interrupt(...)]}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">run 2:  SIDE EFFECT — before interrupt</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        approval resumed with: approve</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        step_two running (approved=approve)</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La misma línea de efecto secundario impresa en la ejecución que pausa y en la que reanuda\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg1-double-side-effect-89c6352b4d9d897ec50519025547fca8.png\" width=\"2214\" height=\"550\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figura 6.</strong> Una aprobación, dos ejecuciones de todo lo que está antes de la interrupción.</p>\n<p>Si esa línea fuera <code>send_email()</code> o <code>charge_card()</code>, una aprobación le cobra al cliente dos\nveces. Nada lanza una excepción. El estado final es correcto. Solo se duplicó el efecto\nsecundario — que es la razón por la que esto es fácil de desplegar y difícil de notar.</p>\n<p><strong>No pongas nada antes de <code>interrupt()</code> en ese nodo.</strong> Mueve los efectos secundarios\ndespués, o dale a la interrupción un nodo propio que no haga nada más. El mismo\nrazonamiento aplica a reanudar tras una caída: un nodo siempre puede ejecutarse más de una\nvez, así que cualquier cosa con una consecuencia externa debería ser idempotente o estar\naislada.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-6--un-modelo-real-y-rastros\">Paso 6 — Un modelo real, y rastros<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/#paso-6--un-modelo-real-y-rastros\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 6 — Un modelo real, y rastros\" title=\"Enlace directo al Paso 6 — Un modelo real, y rastros\" translate=\"no\">​</a></h2>\n<p>Dos paquetes más: uno para hablar con el modelo, otro para trazarlo.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">pip </span><span class=\"token function\" style=\"color:#d73a49\">install</span><span class=\"token plain\"> langchain-openai langfuse</span><br></div></code></pre></div></div>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">llm </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ChatOpenAI</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    model</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"Qwen2.5-3B-Instruct\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    base_url</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"https://inference.wiline.com/v1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    api_key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"WEC_API_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    temperature</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p><code>ChatOpenAI</code> no es específico de OpenAI — habla el protocolo de OpenAI, que WEC Inference\nimplementa. Apuntar <code>base_url</code> ahí es toda la integración. <code>temperature=0</code> mantiene las\nejecuciones comparables mientras pruebas.</p>\n<p>La llamada va dentro de un nodo como cualquier otro trabajo:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">write</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    r </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> llm</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">invoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Write one sentence about </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">state</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'topic'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">.\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"write\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"draft\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>Necesitas un proyecto de Langfuse y un par de claves de API para esto — creados en\n<strong>Organization → Project</strong>, luego <strong>Settings → API Keys</strong>, como cubrimos en\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/\">Observa producción con Langfuse</a>.</p>\n<p>El trazado se adjunta por invocación, en el mismo diccionario de configuración que el id del\nhilo:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langfuse</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">langchain </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> CallbackHandler</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">handler </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> CallbackHandler</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">config </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"configurable\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"thread_id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> THREAD</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"callbacks\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">handler</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p><code>configurable</code> son los ajustes propios de LangGraph; <code>callbacks</code> es el gancho de\nobservadores de LangChain. Langfuse se suscribe a los eventos de inicio y fin de nodo y\nconstruye spans a partir de ellos — que es la razón por la que el rastro refleja tu grafo\nsin ninguna instrumentación extra.</p>\n<p>Las credenciales vienen del entorno — en el SDK v4 <code>CallbackHandler()</code> no acepta\nargumentos, así que esta es la única forma de configurarlo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">WEC_API_KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">'...'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">LANGFUSE_PUBLIC_KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">'pk-lf-...'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">LANGFUSE_SECRET_KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">'sk-lf-...'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">LANGFUSE_HOST</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">'http://localhost:3001'</span><br></div></code></pre></div></div>\n<p>Todo junto como <code>lg_llm.py</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> sys</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> operator </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> add</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> typing </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Annotated</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> typing_extensions </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> TypedDict</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">graph </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> StateGraph</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> START</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> END</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">checkpoint</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">postgres </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> PostgresSaver</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain_openai </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> ChatOpenAI</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langfuse</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">langchain </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> CallbackHandler</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">DB_URI </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"postgresql://langgraph:changeme@127.0.0.1:5434/checkpoints\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">THREAD </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">len</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"draft-1\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">llm </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ChatOpenAI</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    model</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"Qwen2.5-3B-Instruct\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    base_url</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"https://inference.wiline.com/v1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    api_key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"WEC_API_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    temperature</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">TypedDict</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    topic</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    completed</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Annotated</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> add</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    draft</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">plan</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"plan running\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"plan\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">write</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    r </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> llm</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">invoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Write one sentence about </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">state</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'topic'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">.\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"model said:\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"write\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"draft\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> StateGraph</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"plan\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> plan</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"write\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> write</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">START</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"plan\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"plan\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"write\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"write\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> END</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">handler </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> CallbackHandler</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> PostgresSaver</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">from_conn_string</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">DB_URI</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> checkpointer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    checkpointer</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">setup</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    graph </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token builtin\">compile</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">checkpointer</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">checkpointer</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    config </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"configurable\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"thread_id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> THREAD</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"callbacks\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">handler</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    result </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> graph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">invoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"topic\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"edge computing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"draft\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> config</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"FINAL:\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> result</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">python lg_llm.py draft-1</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">LangGraph            2.18s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  plan               0.00s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  write              2.14s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    ChatOpenAI       2.13s   36 -&gt; 37 (73 tokens)</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Rastro de Langfuse con latencia por nodo y conteo de tokens\" src=\"https://development-wec.wiline.com/docs/es/assets/images/lg1-langfuse-trace-8385ab962b94706bb03e2df32a87a0a6.png\" width=\"2526\" height=\"1426\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figura 7.</strong> Árbol de spans, topología del grafo y conteo de tokens — desde un solo\ncallback.</p>\n<p>Casi todo es la llamada al modelo. Eso deja unos 40ms para el grafo y el trazado, y la\ndiferencia se mantuvo cerca de 40ms incluso en ejecuciones que tardaron el doble. <code>plan</code>,\nque no llama a nada, informa 0,00s.</p>\n<p>La conclusión útil: <strong>la orquestación no es tu problema de latencia.</strong> Si un agente se\nsiente lento, el grafo no es la razón.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"resolución-de-problemas-errores-reales\">Resolución de problemas (errores reales)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/#resoluci%C3%B3n-de-problemas-errores-reales\" class=\"hash-link\" aria-label=\"Enlace directo al Resolución de problemas (errores reales)\" title=\"Enlace directo al Resolución de problemas (errores reales)\" translate=\"no\">​</a></h2>\n<p><strong><code>address already in use</code> frente a <code>port is already allocated</code>.</strong> Parecen\nintercambiables y no lo son. El primero significa que un <em>proceso del host</em> tiene el\npuerto; el segundo que lo tiene otro <em>contenedor</em> — la contabilidad propia de Docker, antes\nde intentar el bind. Diagnostica con:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--format</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{{.Names}}\\t{{.Ports}}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> ss </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-ltnp</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> :543</span><br></div></code></pre></div></div>\n<p><strong>Un <code>docker run</code> fallido bloquea el nombre.</strong> El contenedor se crea, luego muere, y sigue\nconservando su nombre — así que tu reintento falla de forma distinta a la primera vez.\n<code>docker rm &lt;nombre&gt;</code> antes de reintentar.</p>\n<p><strong><code>ModuleNotFoundError: No module named 'langchain'</code></strong> al importar el <code>CallbackHandler</code> de\nLangfuse. Ni LangGraph ni <code>langchain-openai</code> necesitan el paquete paraguas <code>langchain</code>;\nLangfuse lo importa puramente como centinela de versión. El arreglo es instalar un paquete\nque nada más en tu stack usa.</p>\n<p>Vale la pena distinguir dos hilos del proyecto.\n<a href=\"https://github.com/langfuse/langfuse/issues/9758\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">#9758</a> reportó el fallo de importación en\noctubre de 2025 y está <strong>cerrado</strong> — pero todavía se reproduce en la versión del SDK de\narriba, así que léelo como historia y no como un arreglo que llegó.\n<a href=\"https://github.com/langfuse/langfuse/issues/13651\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">#13651</a> está <strong>abierto</strong> y propone el\nremedio real: leer <code>langchain_core.__version__</code> en vez de exigir el paquete paraguas.</p>\n<p><strong>Langfuse se desactiva en silencio.</strong> Olvida una variable de entorno y recibes una línea\nde advertencia, y luego una ejecución completamente normal — código de salida 0, salida\ncorrecta, ningún rastro:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Authentication error: Langfuse client initialized without public_key.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Client will be disabled.</span><br></div></code></pre></div></div>\n<p>En el SDK v4 <code>CallbackHandler()</code> no acepta argumentos en el constructor, así que las\nvariables de entorno son la única vía de configuración — y que falte una no lanza ninguna\nexcepción. Comprueba que el cliente está habilitado al arrancar en vez de confiar en la\nausencia de errores.</p>\n<p><strong>Deriva de versiones.</strong> La mayoría del material sobre LangGraph + Langfuse apunta al SDK\nv3 de Langfuse. En v4 el parámetro <code>update_trace</code> ya no existe y lanza <code>TypeError</code>, y las\ncredenciales ya no pueden pasarse al constructor.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>State that survives a restart</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-viene\">Lo que viene<a href=\"https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/#lo-que-viene\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que viene\" title=\"Enlace directo al Lo que viene\" translate=\"no\">​</a></h2>\n<p>Parte 2: pasar trabajo entre agentes — y qué le ocurre al estado compartido cuando no están\nde acuerdo.</p>\n<p>Lecturas relacionadas en este sitio:</p>\n<ul>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/\">Trazado a nivel de componente para llamadas a herramientas de agentes</a> — encontrar qué paso de un agente falló, no solo que falló</li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/\">Autoaloja el agente Hermes con memoria persistente</a> — un modelo de persistencia distinto, memoria entre conversaciones en vez de estado dentro de una ejecución</li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/news/loop-engineering-graph-engineering-what-survives/\">\"Loop Engineering Is Dead\"</a> — por qué la industria pasó de bucles a grafos</li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/langgraph-stateful-agent-wec-instance/",
            "title": "Checkpoints de un agente LangGraph en una instancia WEC para que las caídas no te cuesten nada",
            "summary": "Persiste el estado del agente en Postgres sobre una instancia WEC, llama a un modelo en la API de Inferencia de WEC, y luego mata el proceso a mitad de ejecución dos veces y reanuda ambas desde exactamente donde se detuvo — además de la pausa de aprobación humana, y la interrupción que dispara tus efectos secundarios dos veces en silencio.",
            "date_modified": "2026-08-26T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "agentes",
                "langgraph",
                "state",
                "postgres",
                "langfuse",
                "autoalojamiento",
                "wec",
                "wec-inference"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/litellm-wordmark-86d6f0432720b27534e05bc579dfc29c.png\" alt=\"LiteLLM\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__presidio\" src=\"data:image/webp;base64,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\" alt=\"Presidio\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2255\" height=\"527\" fill=\"none\" viewBox=\"0 0 2255 527\" class=\"tutorialHero__langfuse\"><path fill=\"#1B1917\" d=\"M652.669 116.433c0-10.261-7.683-17.956-17.926-17.956H607V60h87.923v316.366h-42.254zM804.056 379.786c-39.693 0-72.131-27.362-72.131-68.831 0-41.042 28.596-69.686 84.935-69.686h39.693c7.256 0 12.805-5.558 12.805-12.826v-8.55c0-25.651-20.487-38.905-40.547-38.905-18.353 0-33.291 8.123-41.828 26.934h-45.668c11.95-44.462 46.095-65.41 88.349-65.41 40.547 0 82.374 22.231 82.374 77.808v156.046h-42.68v-14.109c0-5.13-5.549-7.695-9.817-4.702-16.646 11.97-32.438 22.231-55.485 22.231m5.975-38.477c18.78 0 34.572-9.406 52.498-27.362 4.695-4.702 6.829-10.688 6.829-17.1v-6.413c0-7.268-5.549-12.826-12.805-12.826H815.58c-27.743 0-40.974 12.398-40.974 31.637 0 17.956 12.377 32.064 35.425 32.064M961.855 376.366V147.642h42.255v19.666c0 5.13 6.4 6.84 10.24 2.992 13.23-12.825 31.59-27.788 60.61-27.788 36.71 0 70.85 23.086 70.85 75.671v158.183h-42.25V227.161c0-29.499-17.93-45.745-39.7-45.745-20.91 0-35 11.971-49.93 29.499-7.26 8.978-9.82 19.238-9.82 30.354v135.097zM1287.48 467c-52.5 0-85.79-24.796-95.61-63.273h46.1c7.25 15.818 20.06 25.651 45.67 25.651 34.14 0 55.48-20.093 55.48-65.838v-7.268c0-5.13-4.27-7.695-9.39-3.42-14.08 12.398-32.01 20.093-49.08 20.093-58.05 0-96.46-44.889-96.46-115.003 0-70.113 44.39-115.43 98.17-115.43 15.79 0 30.3 4.275 44.38 14.963 5.98 4.275 12.38.855 12.38-5.986v-3.847h42.26V363.54c0 72.678-43.97 103.46-93.9 103.46m-2.56-132.959c19.2 0 33.29-8.55 44.81-20.949 7.26-8.122 9.39-13.68 9.39-26.506v-61.563c0-12.826-2.13-20.948-10.67-29.071-9.39-8.978-22.62-14.964-39.69-14.964-35 0-61.89 29.072-61.89 76.954 0 47.883 24.76 76.099 58.05 76.099M1455.92 199.372c0-7.268-5.97-13.253-13.23-13.253h-32.44v-38.477h32.44c7.26 0 13.23-5.985 13.23-13.253v-5.986c0-45.744 23.48-68.403 69.15-68.403h29.02v38.477h-29.45c-17.5 0-26.46 9.833-26.46 29.926v5.986c0 7.268 5.97 13.253 13.23 13.253h42.68v38.477h-42.68c-7.26 0-13.23 5.985-13.23 13.253v176.994h-42.26zM1652.02 381.496c-35.85 0-69.14-23.086-69.14-75.671V147.642h42.25v150.06c0 29.499 17.07 44.889 37.13 44.889 21.77 0 35.85-11.97 50.79-29.499 7.26-8.977 9.82-19.238 9.82-30.354V147.642h42.25v228.724h-42.25V356.7c0-5.131-6.4-6.841-10.24-2.993-13.24 12.826-31.59 27.789-60.61 27.789M1893.57 381.496c-38.84 0-79.39-19.239-90.06-65.838h43.54c6.4 17.528 23.9 29.498 44.81 29.498 23.05 0 37.13-13.68 37.13-30.353 0-16.246-11.09-25.224-28.59-30.354l-36.28-10.261c-31.58-8.978-55.06-29.499-55.06-64.556 0-38.049 35.43-67.12 75.55-67.12 32.01 0 70.85 14.535 81.09 65.41h-40.55c-5.55-17.528-20.06-29.071-40.54-29.071-20.06 0-34.58 12.398-34.58 28.216 0 13.253 8.11 23.086 27.32 28.644l34.15 9.833c32.43 9.406 58.47 29.072 58.47 66.693 0 39.332-34.15 69.259-76.4 69.259M2098.54 381.496c-61.46 0-102.01-51.73-102.01-119.706s43.11-119.278 101.58-119.278c63.6 0 96.89 51.302 96.89 109.872v23.087h-144.26c-5.98 0-8.54 3.847-7.26 13.68 4.7 32.064 30.31 54.295 55.49 54.295 18.78 0 35-9.405 45.67-27.788h44.81c-16.22 40.187-49.94 65.838-90.91 65.838m43.11-141.51c6.83 0 9.39-3.42 7.68-14.108-4.69-26.506-24.33-45.317-51.22-45.317-25.6 0-47.37 18.811-54.2 45.745-2.56 9.833.85 13.68 6.83 13.68z\"></path><path fill=\"#FF5D5F\" d=\"m286.292 286.105 34.597 27.791s26.473-19.661 45.941-22.545c20.418-3.025 42.202 8.359 62.388 21.93 30.489 20.498 56.149 46.508 56.149 46.508l30.06-29.493s-82.879-89.795-148.597-81.672c-43.105 5.328-80.538 37.481-80.538 37.481\"></path><path fill=\"#4E9CFF\" d=\"M88.358 114.862 60 146.056s79.009 73.732 141.224 73.732c28.358 0 67.684-22.216 101.523-51.079 19.283-16.448 40.835-35.13 62.388-35.13 14.487 0 33.594 7.673 51.612 27.824 0 0 11.63-6.974 18.716-11.985 6.228-4.404 15.479-11.91 15.479-11.91-25.918-27.663-63.407-47.883-85.807-45.9-36.299.005-62.388 22.601-94.717 48.735s-45.94 36.907-69.194 36.907c-39.134 0-112.866-62.388-112.866-62.388M88.358 352.463 60 321.269s79.009-73.732 141.224-73.732c28.358 0 67.684 22.216 101.523 51.079 19.283 16.448 40.835 35.13 62.388 35.13 14.556 0 33.518-7.989 51.612-28.358 0 0 10.877 6.705 17.582 11.344 6.894 4.769 17.015 12.655 17.015 12.655-25.931 27.883-63.693 48.323-86.209 46.33-36.299-.005-57.851-19.24-90.179-45.374-32.329-26.133-50.478-40.268-73.732-40.268-39.134 0-112.866 62.388-112.866 62.388M458.142 185.149c-7.378 5.1-19.283 12.478-19.283 12.478s6.806 14.746 6.806 34.597-6.239 36.866-6.239 36.866 10.688 6.675 17.582 11.343c7.162 4.849 18.149 13.045 18.149 13.045s13.045-27.224 13.045-61.254-13.045-59.552-13.045-59.552-10.236 7.792-17.015 12.477\"></path><path fill=\"#FF5D5F\" d=\"m287.995 180.612 32.895-27.224s26.473 19.046 45.941 21.93c20.417 3.026 42.202-8.359 62.388-21.93 30.489-20.498 56.149-46.507 56.149-46.507l30.06 29.492s-82.879 89.795-148.597 81.672c-43.105-5.328-78.836-37.433-78.836-37.433M208.601 91c42.538 0 78.264 36.299 78.264 36.299s-9.941 7.832-16.448 13.045c-6.777 5.429-17.582 14.179-17.582 14.179s-18.711-19.851-44.234-19.851c-10.465 0-24.066 6.286-38.567 18.716-11.188 9.591-22.829 21.514-30.627 36.299-6.743 12.784-10.42 27.85-10.776 43.672-.446 19.873 6.597 40.704 18.149 57.283 7.743 11.112 16.983 19.474 26.657 26.657 12.555 9.322 25.648 15.881 35.164 15.881 10.166 0 19.306-3.533 26.09-6.806 10.776-6.239 19.278-13.612 19.278-13.612l33.463 27.791s-13.612 13.612-32.323 23.821c-12.091 5.963-27.632 11.91-46.508 11.91-18.862 0-40.767-10.022-61.254-25.522-13.244-10.021-26.225-21.895-36.298-36.299-16.51-23.607-25.017-52.328-24.96-81.104.057-29.136 9.451-57.993 26.094-81.672C138.273 117.657 176.86 91 208.601 91\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 5 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->5</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->5<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting an LLM gateway</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">One endpoint, scoped keys</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Route work to the right model</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Mask PII at the gateway</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">4</span><span class=\"skillTracker__skill\" data-state=\"current\">Clean traces, untouched answers</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/load-test-llm-gateway-blocking-callback/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove it holds under load</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/\">La Parte 3</a> configuró Presidio para enmascarar\nlos prompts en el momento del registro y deliberadamente se detuvo antes de probarlo. Aún no se había\nconectado a un destino de registro, así que no había rastro que inspeccionar.</p>\n<p>Esta publicación lo conecta, encuentra <code>&lt;PERSON&gt;</code> donde debería estar — y luego encuentra la\ndirección de correo electrónico real del cliente sentada unas líneas más abajo, en la respuesta del modelo.</p>\n<!-- -->\n<!-- -->\n<!-- -->\n<p>Todo en esta publicación sigue desde donde se encuentran esos dos hooks. <code>post_call</code> se activa\n<em>antes</em> de la flecha de regreso al llamador, así que enmascarar allí reescribe la respuesta que\nreciben. <code>async_logging_hook</code> se activa <em>después</em> de eso, solo en la rama hacia el registrador.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"enviando-el-tráfico-del-gateway-a-langfuse\">Enviando el tráfico del gateway a Langfuse<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/#enviando-el-tr%C3%A1fico-del-gateway-a-langfuse\" class=\"hash-link\" aria-label=\"Enlace directo al Enviando el tráfico del gateway a Langfuse\" title=\"Enlace directo al Enviando el tráfico del gateway a Langfuse\" translate=\"no\">​</a></h2>\n<p>Langfuse ya está funcionando en esta serie; si seguiste\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/\">el tutorial de observabilidad</a> tienes una instancia. Crea un proyecto para el gateway, luego toma sus dos claves de\n<strong>Settings → API Keys</strong>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La pantalla de configuración del proyecto de Langfuse mostrando las claves secreta y pública\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw4-langfuse-keys-0868edc9108438174e185d84a9801382.png\" width=\"1905\" height=\"992\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figura 1.</strong> Un proyecto separado mantiene el tráfico del gateway alejado de cualquier otra cosa\nque estés rastreando.</p>\n<p>Agrega tres variables al <code>.env</code> del gateway:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">LANGFUSE_PUBLIC_KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">pk-lf-</span><span class=\"token punctuation\" style=\"color:#393A34\">..</span><span class=\"token plain\">.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">LANGFUSE_SECRET_KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">sk-lf-</span><span class=\"token punctuation\" style=\"color:#393A34\">..</span><span class=\"token plain\">.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">LANGFUSE_HOST</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">http://your-host:3001</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>El nombre de la variable no es el que Langfuse te da</div><div class=\"admonitionContent_BuS1\"><p>La propia pantalla de configuración de Langfuse imprime un bloque <code>.env</code> que contiene\n<code>LANGFUSE_BASE_URL</code>. El gateway no lee eso. Lee:</p><div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">getenv</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"LANGFUSE_HOST\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"https://cloud.langfuse.com\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div><p>Copia su fragmento tal cual y tu host será ignorado a favor del endpoint de la nube pública — donde tus claves autoalojadas no se autenticarán. Los rastros no van a ninguna parte, y no hay errores.</p></div></div>\n<p>Luego activa el callback:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">litellm_settings</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">drop_params</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean important\" style=\"color:#36acaa\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">success_callback</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"langfuse\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">failure_callback</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"langfuse\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"reiniciar-no-es-suficiente\">Reiniciar no es suficiente<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/#reiniciar-no-es-suficiente\" class=\"hash-link\" aria-label=\"Enlace directo al Reiniciar no es suficiente\" title=\"Enlace directo al Reiniciar no es suficiente\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose restart litellm</span><br></div></code></pre></div></div>\n<p>Envía una solicitud después de eso y los registros dicen:</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\">El cliente de Langfuse está deshabilitado ya que no se proporcionó public_key como parámetro</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">o variable de entorno 'LANGFUSE_PUBLIC_KEY'.</span><br></div></code></pre></div></div>\n<p>Las claves están en <code>.env</code>. No están en el contenedor:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> llm-gateway </span><span class=\"token function\" style=\"color:#d73a49\">printenv</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> LANGFUSE</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">0</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span><code>restart</code> no recarga <code>env_file</code></div><div class=\"admonitionContent_BuS1\"><p><code>docker compose restart</code> reinicia el <em>proceso</em> dentro del contenedor que\nya existe, con el entorno con el que fue creado. Las nuevas variables en\n<code>.env</code> no se recogen.</p><p>El cambio en <code>config.yaml</code> <em>sí</em> tuvo efecto, porque ese es un archivo montado que se lee al inicio. Así que el gateway parece estar configurado correctamente — callbacks inicializados, sin errores — mientras no tiene credenciales en absoluto.</p><p>Usa <code>docker compose up -d</code>, que recrea el contenedor.</p></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> llm-gateway </span><span class=\"token function\" style=\"color:#d73a49\">printenv</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> LANGFUSE</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">3</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"printenv dentro del contenedor ahora reportando tres variables LANGFUSE\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw4-recreate-env-8aff004f8b60f6c931690f76127937bf.png\" width=\"363\" height=\"71\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figura 2.</strong> Después de <code>up -d</code>, las variables están en el contenedor.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-rastro-llega-y-el-ciclo-se-cierra\">El rastro llega, y el ciclo se cierra<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/#el-rastro-llega-y-el-ciclo-se-cierra\" class=\"hash-link\" aria-label=\"Enlace directo al El rastro llega, y el ciclo se cierra\" title=\"Enlace directo al El rastro llega, y el ciclo se cierra\" translate=\"no\">​</a></h2>\n<p>Espera a que <code>Application startup complete</code>, luego envía un prompt que lleva un nombre y\nuna dirección:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">source</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$LITELLM_MASTER_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-small\",\"messages\":[{\"role\":\"user\",\"content\":\"¿Cuál es el primer nombre de Maria Alvarez, y cuál es el dominio de maria.alvarez@example.com? Responde en una línea corta.\"}],\"max_tokens\":40}'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.choices[0].message.content'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">El primer nombre de Maria Alvarez es Maria; el dominio de maria.alvarez@example.com es example.com.</span><br></div></code></pre></div></div>\n<p>Esa respuesta es el control. El modelo no podría haber producido \"Maria\" de un\nplaceholder, así que la solicitud en vivo llegó a él sin enmascarar — exactamente lo que\npromete <code>logging_only</code>.</p>\n<p>Ahora el rastro:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Un rastro de Langfuse mostrando la entrada enmascarada sobre la respuesta sin enmascarar del asistente\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw4-trace-input-masked-f167939e2680fdb4984817374cb86a8e.png\" width=\"1899\" height=\"989\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figura 3.</strong> Entrada enmascarada. Salida no.</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Stored input</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">¿Cuál es el primer nombre de &lt;PERSON&gt;, y cuál es el dominio de &lt;EMAIL_ADDRESS&gt;?</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Stored output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">El primer nombre de Maria Alvarez es Maria; el dominio de maria.alvarez@example.com es example.com.</span><br></div></code></pre></div></div>\n<p>La tarea de la Parte 3 está hecha — el prompt está enmascarado en el registro mientras el modelo vio el\ntexto real. Y la PII está en el rastro de todos modos, porque el modelo la repitió.</p>\n<p>Eso no es un caso extremo. Repetir el nombre del cliente es para lo que sirve un asistente de soporte.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-registro-de-gastos-es-peor\">El registro de gastos es peor<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/#el-registro-de-gastos-es-peor\" class=\"hash-link\" aria-label=\"Enlace directo al El registro de gastos es peor\" title=\"Enlace directo al El registro de gastos es peor\" translate=\"no\">​</a></h2>\n<p>La propia base de datos del gateway almacena la misma solicitud de manera diferente:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/spend/logs </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$LITELLM_MASTER_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token string\" style=\"color:#e3116c\">'.[0] | {messages, response}'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">\"messages\": {}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">\"response\": { ... \"content\": \"El primer nombre de Maria Alvarez es Maria;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">              el dominio de maria.alvarez@example.com es example.com.\" ... }</span><br></div></code></pre></div></div>\n<p>Sin prompt en absoluto, y la finalización en su totalidad. Dos almacenes, dos formas, el mismo\nhueco en ambos.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"ampliar-el-alcance-intercambia-un-problema-por-otro\">Ampliar el alcance intercambia un problema por otro<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/#ampliar-el-alcance-intercambia-un-problema-por-otro\" class=\"hash-link\" aria-label=\"Enlace directo al Ampliar el alcance intercambia un problema por otro\" title=\"Enlace directo al Ampliar el alcance intercambia un problema por otro\" translate=\"no\">​</a></h2>\n<p>La Parte 3 estableció <code>presidio_filter_scope: input</code>. La solución obvia es escanear en ambas\ndirecciones:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">presidio_filter_scope</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> both</span><br></div></code></pre></div></div>\n<p>Reinicia, envía la solicitud idéntica y lee la respuesta que recibe el llamador:</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\">El primer nombre de &lt;PERSON&gt; es &lt;PERSON&gt;; el dominio de &lt;EMAIL_ADDRESS&gt; es &lt;URL&gt;.</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La respuesta de curl que contiene placeholders en lugar del nombre y la dirección reales\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw4-response-masked-5e5f2e3cd767089449d5067827a9f518.png\" width=\"390\" height=\"192\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figura 4.</strong> La propia terminal del llamador, no un visor de registros.</p>\n<p>Eso no es el registro. Esa es la respuesta devuelta al cliente.</p>\n<p><code>mode: logging_only</code> sigue configurado. No ayudó, porque nunca llega a la\nruta de salida — la documentación para la configuración del alcance lo dice directamente:</p>\n<blockquote>\n<p>Usa <code>presidio_filter_scope: output</code> (o <code>both</code>) cuando quieras que Presidio\nescanee y enmascare activamente la respuesta del modelo <strong>antes de que llegue al usuario</strong>.</p>\n</blockquote>\n<p>Enmascarar la respuesta en vivo es el propósito documentado de un alcance de salida. Lo que no está documentado es la combinación: nada describe lo que sucede cuando configuras\n<code>logging_only</code> <em>y</em> un alcance de salida, y el código lo resuelve a favor de la\nruta en vivo.</p>\n<p>En <code>guardrail_initializers.py</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> run_output</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    output_callback </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> _make_presidio_callback</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        apply_to_output</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        event_hook</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">GuardrailEventHooks</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">post_call</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">value</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\">   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># codificado</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        output_parse_pii</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">False</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>El <code>mode</code> que configuraste se pasa al callback de <em>entrada</em>. El callback de salida\nrecibe <code>post_call</code> independientemente.</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>Verifica que el modelo aún reciba datos reales</div><div class=\"admonitionContent_BuS1\"><p>Una respuesta enmascarada tiene dos posibles causas: la respuesta fue enmascarada en el camino de salida,\no el modelo recibió placeholders y respondió honestamente sobre ellos. Se ven idénticos.</p><p>Pide algo que codifique el valor real sin contenerlo — una sola letra no es PII, así que enmascarar no puede ocultarlo:</p><div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">..</span><span class=\"token plain\">. </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-small\",\"messages\":[{\"role\":\"user\",\"content\":\"Responde SOLO con la primera letra del primer nombre de Maria Alvarez.\"}],\"max_tokens\":200}'</span><br></div></code></pre></div></div><div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Mª</span><br></div></code></pre></div></div><p><code>M</code>. El modelo tenía el nombre real. Solo se reescribió la respuesta.</p></div></div>\n<p>Así que las dos configuraciones te dan una elección, y ninguna es la que quieres:</p>\n<table><thead><tr><th></th><th>Entrada registrada</th><th>Salida registrada</th><th>Respuesta del llamador</th></tr></thead><tbody><tr><td><code>filter_scope: input</code></td><td>enmascarada</td><td><strong>PII sin procesar</strong></td><td>intacta</td></tr><tr><td><code>filter_scope: both</code></td><td>enmascarada</td><td>enmascarada</td><td><strong>enmascarada</strong></td></tr></tbody></table>\n<p>Esto ha sido reportado.\n<a href=\"https://github.com/BerriAI/litellm/issues/30447\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Issue #30447</a> — <em>\"logging_only\nEl guardrail de Presidio corrompe la respuesta orientada al usuario\"</em> — fue presentado el 15 de junio de 2026\ny cerrado al día siguiente, sin discusión, junto con una solicitud de extracción titulada\n<em>\"fix(presidio): no enmascarar la solicitud en vivo cuando el guardrail está en logging_only\"</em>.\nEl informe era sobre la respuesta. La solución abordó la solicitud.\n<a href=\"https://github.com/BerriAI/litellm/issues/35951\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">#35951</a> sigue abierta en un\ncamino relacionado.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"haciéndolo-en-el-momento-del-registro-en-su-lugar\">Haciéndolo en el momento del registro en su lugar<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/#haci%C3%A9ndolo-en-el-momento-del-registro-en-su-lugar\" class=\"hash-link\" aria-label=\"Enlace directo al Haciéndolo en el momento del registro en su lugar\" title=\"Enlace directo al Haciéndolo en el momento del registro en su lugar\" translate=\"no\">​</a></h2>\n<p>La brecha existe porque <code>post_call</code> se ejecuta mientras la respuesta aún viaja\nhacia el llamador. Hay un hook posterior que no lo hace.</p>\n<p><code>async_logging_hook</code> se activa después de que el modelo ha respondido y antes de que se ejecuten los registradores. Recibe la solicitud y el resultado, y <strong>lo que devuelve es lo que se registra</strong>. El llamador ya tiene su respuesta para entonces.</p>\n<p>Ese hook está documentado, bajo\n<a href=\"https://docs.litellm.ai/docs/observability/scrub_data\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Scrub Logged Data</a> — pero\nel ejemplo allí es un placeholder que reemplaza cada mensaje con la cadena literal <code>MASK_THIS_ASYNC_VALUE</code>. Muestra que el hook existe; no enmascara nada, solo toca <code>kwargs[\"messages\"]</code>, y muta en su lugar. Lo que sigue utiliza el mismo punto de extensión y agrega las partes que lo hacen funcionar:\nllamadas reales a Presidio, los tres lugares donde se oculta la PII, y una copia para que la respuesta del llamador sobreviva.</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>Su ejemplo no se ejecuta como está escrito</div><div class=\"admonitionContent_BuS1\"><p>El fragmento en esa página termina con <code>return kwargs, responses</code>, pero el parámetro se llama <code>result</code>. <code>responses</code> no está definido, así que copiarlo tal cual genera un <code>NameError</code>.</p></div></div>\n<p>Crea <code>scrubber.py</code> junto a <code>config.yaml</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">scrubber.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> copy</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> os</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> typing </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Any</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> Tuple</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> httpx</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> litellm</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">integrations</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">custom_logger </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> CustomLogger</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ANALYZER </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">getenv</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"PRESIDIO_ANALYZER_API_BASE\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://presidio-analyzer:3000\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ANONYMIZER </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">getenv</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"PRESIDIO_ANONYMIZER_API_BASE\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://presidio-anonymizer:3000\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">_mask</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">text</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> text </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> text</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">strip</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> text</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> httpx</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">AsyncClient</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">timeout</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">10.0</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        r </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">post</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">ANALYZER</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">/analyze\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> json</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"text\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> text</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"language\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"en\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        found </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">json</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> found</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> text</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        r </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">post</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">ANONYMIZER</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">/anonymize\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            json</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"text\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> text</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"analyzer_results\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> found</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">json</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"text\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">PresidioLogScrubber</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">CustomLogger</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">async_logging_hook</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        self</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> kwargs</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> result</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Any</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> call_type</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> Tuple</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> Any</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> call_type </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"completion\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"acompletion\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> kwargs</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> result</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        kwargs </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> copy</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">deepcopy</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">kwargs</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> message </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> kwargs</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">isinstance</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                message</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> _mask</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        slo </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> kwargs</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"standard_logging_object\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">isinstance</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">slo</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> message </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> slo</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">isinstance</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">and</span><span class=\"token plain\"> </span><span class=\"token builtin\">isinstance</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                    message</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> _mask</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            response </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> slo</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"response\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">isinstance</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">response</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> choice </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> response</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"choices\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                    message </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">choice </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"message\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">isinstance</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                        message</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> _mask</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># `result` es el objeto que ya tiene el llamador. Copiar antes de enmascarar.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        logged_result </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> copy</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">deepcopy</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">result</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> choice </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token builtin\">getattr</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">logged_result</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"choices\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            message </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token builtin\">getattr</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">choice</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"message\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> message </span><span class=\"token keyword\" style=\"color:#00009f\">is</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">and</span><span class=\"token plain\"> </span><span class=\"token builtin\">isinstance</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token builtin\">getattr</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                message</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> _mask</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> kwargs</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> logged_result</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">instance </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> PresidioLogScrubber</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Tres detalles llevan todo esto.</p>\n<p><strong>La PII se oculta en tres lugares, no en uno.</strong> <code>kwargs[\"messages\"]</code> es el prompt.\n<code>kwargs[\"standard_logging_object\"]</code> es lo que las integraciones de registro realmente\nleen — este es el tema del issue #35951. Y <code>result</code> contiene la finalización.\nPerder cualquiera de ellos significa que algo se filtra.</p>\n<p><strong><code>result</code> es el objeto del llamador.</strong> Mutarlo en su lugar reproduce el error que\nestamos evitando. Se copia profundamente primero, y la copia es lo que enmascaramos y\ndevolvemos.</p>\n<p><strong><code>_mask</code> es async a propósito.</strong> Más sobre eso a continuación, porque hacerlo mal\ncuesta latencia medible.</p>\n<p>Monta el archivo y regístralo. Debe estar al lado de <code>config.yaml</code>, porque\n<code>get_instance_fn</code> resuelve la ruta con puntos relativa al directorio del archivo de configuración:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">docker-compose.yml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">volumes</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> ./config.yaml</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/app/config.yaml</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">ro</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> ./scrubber.py</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/app/scrubber.py</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">ro</span><br></div></code></pre></div></div>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">litellm_settings</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">drop_params</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean important\" style=\"color:#36acaa\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">callbacks</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scrubber.instance\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">success_callback</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"langfuse\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><br></div></code></pre></div></div>\n<p>Establece el guardrail incorporado en <code>default_on: false</code> mientras pruebas. Si tanto él como\nel callback están enmascarando, no puedes decir cuál hizo el trabajo.</p>\n<p>Agregar un volumen cambia la definición del contenedor, así que esto necesita una recreación, no\nun reinicio:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"probándolo-en-ambos-lados-de-una-solicitud\">Probándolo, en ambos lados de una solicitud<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/#prob%C3%A1ndolo-en-ambos-lados-de-una-solicitud\" class=\"hash-link\" aria-label=\"Enlace directo al Probándolo, en ambos lados de una solicitud\" title=\"Enlace directo al Probándolo, en ambos lados de una solicitud\" translate=\"no\">​</a></h2>\n<p>Envía una solicitud y guarda el cuerpo de la respuesta:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\":\"¿Quién es Priya Raghunathan y cuál es el dominio de priya.raghunathan@example.net? Una línea corta.\"}],\"max_tokens\":40}'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> response.json</span><br></div></code></pre></div></div>\n<p>Lo que recibió el llamador:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.choices[0].message.content'</span><span class=\"token plain\"> response.json</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Priya Raghunathan es una investigadora de IA, y el dominio</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">priya.raghunathan@example.net es probablemente una dirección de correo electrónico personal.</span><br></div></code></pre></div></div>\n<p>Lo que almacenó Langfuse:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-u</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$LANGFUSE_PUBLIC_KEY</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string variable\" style=\"color:#36acaa\">$LANGFUSE_SECRET_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token string\" style=\"color:#e3116c\">\"http://your-host:3001/api/public/traces?limit=1\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> trace.json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.data[0].output.content'</span><span class=\"token plain\"> trace.json</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">&lt;PERSON&gt; es una investigadora de IA, y el dominio &lt;EMAIL_ADDRESS&gt; es probablemente una</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">dirección de correo electrónico personal.</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La misma oración dos veces — la respuesta con valores reales, el rastro con placeholders\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw4-side-by-side-54fd1e85dfe8ba2b6544c1a470fb77f9.png\" width=\"448\" height=\"317\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figura 5.</strong> La misma oración. Una real, una enmascarada.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El rastro de Langfuse con la entrada y salida enmascaradas, junto a la respuesta sin enmascarar\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw4-scrubber-working-29c3b666ef2aa43daef02208253f2fd4.png\" width=\"1901\" height=\"981\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figura 6.</strong> Enmascarado en ambos lados del rastro, intacto para el llamador.</p>\n<p>Leer una UI renderizada prueba menos que verificar las cargas útiles, así que verifícalas:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Priya Raghunathan\"</span><span class=\"token plain\"> response.json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Priya Raghunathan\"</span><span class=\"token plain\"> trace.json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"PERSON\"</span><span class=\"token plain\"> trace.json </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">wc</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-l</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Una línea corta\"</span><span class=\"token plain\"> trace.json</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">1</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Cuatro conteos de grep que prueban que la respuesta contiene el nombre real y el rastro no\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw4-grep-proof-f38350595a637c78223db0c9f01b0da1.png\" width=\"448\" height=\"177\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figura 7.</strong> El recibo: <code>1 0 2 1</code>.</p>\n<p>El nombre real está en lo que recibió el llamador. No está en el rastro. El\nplaceholder aparece dos veces — entrada y salida. Y la última línea prueba que ambos\narchivos describen la <em>misma</em> solicitud, lo que los primeros tres no establecen por sí solos.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes enmascarar lo que un gateway registra sin cambiar lo que devuelve, y probarlo\ninspeccionando las cargas útiles en ambos lados de una sola solicitud.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-versión-que-te-cuesta-silenciosamente-200-350ms\">La versión que te cuesta silenciosamente 200-350ms<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/#la-versi%C3%B3n-que-te-cuesta-silenciosamente-200-350ms\" class=\"hash-link\" aria-label=\"Enlace directo al La versión que te cuesta silenciosamente 200-350ms\" title=\"Enlace directo al La versión que te cuesta silenciosamente 200-350ms\" translate=\"no\">​</a></h2>\n<p>La primera versión funcional de <code>_mask</code> usó un cliente bloqueante — <code>httpx.Client</code>\ndentro de un <code>async def</code>. Enmascaró correctamente. También hizo esto:</p>\n<table><thead><tr><th></th><th>ejecución 1</th><th>ejecución 2</th></tr></thead><tbody><tr><td>sin callback</td><td>0.74 s</td><td>—</td></tr><tr><td>bloqueando <code>httpx.Client</code></td><td>1.10 s</td><td>0.97 s</td></tr><tr><td>async <code>httpx.AsyncClient</code></td><td>0.77 s</td><td>0.77 s</td></tr></tbody></table>\n<p>Medianas cálidas, cuatro solicitudes secuenciales cada una, la primera descartada como fría. La figura asíncrona se repitió exactamente; la bloqueante no, que es el punto — la penalización depende de lo que más esté haciendo el bucle.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Ejecuciones cronometradas con el cliente bloqueante y con el cliente asíncrono\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw4-timing-97e67eb7eb08fb9770f75d0b038bb147.png\" width=\"386\" height=\"224\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figura 8.</strong> La misma solicitud, el mismo enmascaramiento, una palabra diferente en el código.</p>\n<p>Una llamada HTTP bloqueante dentro de un hook asíncrono detiene el bucle de eventos hasta que Presidio\nresponde, y cada solicitud hace varias de tales llamadas. El enmascaramiento no está en la ruta crítica del\nllamador — pero está en la de todos los demás, porque nada más puede ser atendido mientras espera.</p>\n<p>Cambiar <code>httpx.Client</code> a <code>httpx.AsyncClient</code> y esperar las llamadas elimina\nel costo: 0.77s contra una línea base de 0.74s, dentro del ruido de la llamada del modelo\nen sí, y se reprodujo hasta el centésimo en dos ejecuciones separadas.</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>Lo que no probamos</div><div class=\"admonitionContent_BuS1\"><p>Estos números son cuatro solicitudes secuenciales. Bloquear el bucle de eventos apenas\nse muestra en una solicitud a la vez — es bajo concurrencia que las dos versiones\ndivergen, y no hemos medido eso aquí. Si ejecutas esto a volumen, pruébalo bajo carga\nantes de confiar en la tabla anterior.</p><p>Cada solicitud también significa dos llamadas HTTP a Presidio por mensaje más dos para la\nfinalización. A volumen real, Presidio se convierte en un servicio que dimensionas y monitoreas,\nno un detalle de fondo.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"dónde-te-deja-esto\">Dónde te deja esto<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/#d%C3%B3nde-te-deja-esto\" class=\"hash-link\" aria-label=\"Enlace directo al Dónde te deja esto\" title=\"Enlace directo al Dónde te deja esto\" translate=\"no\">​</a></h2>\n<table><thead><tr><th></th><th>Entrada registrada</th><th>Salida registrada</th><th>Respuesta del llamador</th><th>Costo</th></tr></thead><tbody><tr><td><code>filter_scope: input</code></td><td>enmascarada</td><td>PII sin procesar</td><td>intacta</td><td>ninguno</td></tr><tr><td><code>filter_scope: both</code></td><td>enmascarada</td><td>enmascarada</td><td>enmascarada</td><td>0.30s de duración del guardrail</td></tr><tr><td>hook de registro</td><td>enmascarada</td><td>enmascarada</td><td>intacta</td><td>ninguno medible</td></tr></tbody></table>\n<p>Vale la pena ser claro sobre el límite de lo que esto logra: la PII aún viaja.\nLlega al modelo y llega al llamador. Lo que se ha eliminado es\n<em>retención</em> — ya no está sentada en un almacén de rastros que un grupo más amplio\nde personas puede leer, meses después, mucho después de que la solicitud misma se haya ido.</p>\n<p>Verificado contra <code>ghcr.io/berriai/litellm:main-stable</code>, UI de administración reportando\nv1.96.2, el 21 de agosto de 2026. Si una versión posterior agrega un modo de salida con\nalcance de registro, prefierelo sobre esto.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-de-problemas\">Solución de problemas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/#soluci%C3%B3n-de-problemas\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas\" title=\"Enlace directo al Solución de problemas\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"los-rastros-nunca-aparecen-y-nada-da-error\">Los rastros nunca aparecen y nada da error<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/#los-rastros-nunca-aparecen-y-nada-da-error\" class=\"hash-link\" aria-label=\"Enlace directo al Los rastros nunca aparecen y nada da error\" title=\"Enlace directo al Los rastros nunca aparecen y nada da error\" translate=\"no\">​</a></h3>\n<p>Verifica que <code>LANGFUSE_HOST</code> esté configurado, no <code>LANGFUSE_BASE_URL</code>, y confirma que las variables\nestén dentro del contenedor con <code>docker exec llm-gateway printenv | grep LANGFUSE</code>.\nUn <code>docker compose restart</code> no las habrá cargado.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-callback-no-se-ejecuta-y-el-gateway-inicia-normalmente\">El callback no se ejecuta y el gateway inicia normalmente<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/#el-callback-no-se-ejecuta-y-el-gateway-inicia-normalmente\" class=\"hash-link\" aria-label=\"Enlace directo al El callback no se ejecuta y el gateway inicia normalmente\" title=\"Enlace directo al El callback no se ejecuta y el gateway inicia normalmente\" translate=\"no\">​</a></h3>\n<p>Una mala ruta con puntos falla silenciosamente. <code>scrubber.py</code> debe estar en el mismo directorio que\n<code>config.yaml</code> — dentro del contenedor, no solo en el host — y el nombre del objeto\ndespués del punto debe existir. Busca en el registro de inicio <code>ImportError</code> y\n<code>AttributeError</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-respuesta-vuelve-enmascarada\">La respuesta vuelve enmascarada<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/#la-respuesta-vuelve-enmascarada\" class=\"hash-link\" aria-label=\"Enlace directo al La respuesta vuelve enmascarada\" title=\"Enlace directo al La respuesta vuelve enmascarada\" translate=\"no\">​</a></h3>\n<p>La copia profunda falta, o el guardrail incorporado sigue habilitado con un\nalcance de salida. Establece <code>default_on: false</code> en él y confirma con\n<code>curl /guardrails/list</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"todo-se-volvió-más-lento\">Todo se volvió más lento<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/#todo-se-volvi%C3%B3-m%C3%A1s-lento\" class=\"hash-link\" aria-label=\"Enlace directo al Todo se volvió más lento\" title=\"Enlace directo al Todo se volvió más lento\" translate=\"no\">​</a></h3>\n<p>Verifica que <code>_mask</code> use <code>httpx.AsyncClient</code> con <code>await</code>, no <code>httpx.Client</code>. Consulta las\nmediciones anteriores.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Clean traces, untouched answers</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-sigue\">Qué sigue<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al Qué sigue\" title=\"Enlace directo al Qué sigue\" translate=\"no\">​</a></h2>\n<p>Las mediciones aquí son secuenciales, y el modo de falla que costó 200-350ms\nes uno que solo afecta adecuadamente bajo concurrencia. La próxima publicación pone el gateway\nbajo carga paralela y mide lo que realmente sucede con la latencia cuando varias\nsolicitudes compiten por el mismo bucle de eventos.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lectura-adicional\">Lectura adicional<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/#lectura-adicional\" class=\"hash-link\" aria-label=\"Enlace directo al Lectura adicional\" title=\"Enlace directo al Lectura adicional\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.litellm.ai/docs/proxy/guardrails/pii_masking_v2\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Enmascaramiento de PII, PHI — Presidio</a></li>\n<li class=\"\"><a href=\"https://docs.litellm.ai/docs/observability/scrub_data\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Scrub Logged Data</a> — el hook documentado en el que se basa esta publicación</li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/\">Parte 3 — enmascarar PII en el gateway</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/",
            "title": "Rastros limpios, respuestas intactas: enmascarando PII en los registros de LiteLLM sin corromper la respuesta",
            "summary": "Enviar el tráfico de un gateway a Langfuse y la respuesta del modelo lleva la PII de vuelta a tus rastros. Ampliar el alcance de Presidio y enmascara la respuesta que reciben tus usuarios en su lugar. Ninguna de las configuraciones te da ambas cosas, así que aquí hay un callback de cuarenta líneas que lo hace — medido, y con el error que lo hace más lento.",
            "date_modified": "2026-08-21T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "llm",
                "gateway",
                "pii",
                "privacy",
                "guardrails",
                "observabilidad",
                "langfuse",
                "litellm",
                "presidio",
                "wec"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/litellm-wordmark-86d6f0432720b27534e05bc579dfc29c.png\" alt=\"LiteLLM\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__presidio\" src=\"data:image/webp;base64,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\" alt=\"Presidio\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 5 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->5</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->5<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting an LLM gateway</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">One endpoint, scoped keys</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Route work to the right model</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">3</span><span class=\"skillTracker__skill\" data-state=\"current\">Mask PII at the gateway</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Clean traces, untouched answers</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/load-test-llm-gateway-blocking-callback/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove it holds under load</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/\">La Parte 2</a> terminó con la puerta de enlace decidiendo\nqué modelo responde a cada solicitud — y aún así reenvía cada solicitud tal cual,\nincluyendo las que llevan nombres de clientes, correos electrónicos y números de teléfono.</p>\n<p>Esta publicación coloca un filtro de PII en ese camino. No delante del modelo, sin embargo:\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/news/mask-your-logs-not-your-prompts/\">delante de los registros</a>. El modelo que lee\ntu solicitud está haciendo su trabajo; el riesgo es lo que se <strong>almacena</strong>. Así que el texto sin procesar\nva al modelo y una copia enmascarada va a tus registros.</p>\n<p>Eso es lo que la puerta de enlace publicita. Seguir su configuración documentada me dio\nlo opuesto: un modelo que respondió correctamente y una respuesta que regresó como\n<code>&lt;LOCATION&gt;</code>. Así es como encontrar eso y cómo solucionarlo.</p>\n<!-- -->\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Versión probada</div><div class=\"admonitionContent_BuS1\"><p>LiteLLM <code>1.96.2</code> (<code>ghcr.io/berriai/litellm:main-stable</code>), imágenes de Presidio de\n<code>ghcr.io/data-privacy-stack</code>, modelos <code>qwen-small</code> (Qwen2.5-3B-Instruct) y\n<code>qwen-mid</code> (Qwen3.5-9B) a través de la API de Inferencia WEC, el 20 de agosto de 2026. Los valores predeterminados\ncambian — si tus resultados difieren, verifica primero tu versión.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-es-presidio-y-dónde-se-coloca\">Qué es Presidio y dónde se coloca<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#qu%C3%A9-es-presidio-y-d%C3%B3nde-se-coloca\" class=\"hash-link\" aria-label=\"Enlace directo al Qué es Presidio y dónde se coloca\" title=\"Enlace directo al Qué es Presidio y dónde se coloca\" translate=\"no\">​</a></h2>\n<p><a href=\"https://github.com/data-privacy-stack/presidio\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Presidio</a> detecta datos personales\nen texto: nombres, correos electrónicos, números de teléfono, números de tarjeta. Le envías una cadena y\ndevuelve las entidades que encontró, con desplazamientos de caracteres y un puntaje de confianza.</p>\n<p>Se envía como dos servicios: un <strong>analizador</strong> que encuentra PII y un <strong>anonimizador</strong>\nque lo reemplaza. La puerta de enlace llama a ambos.</p>\n<p>Una nota sobre nombres que confunde a la gente: Presidio era el proyecto de Microsoft y la mayoría\nde los tutoriales aún lo llaman así. El repositorio se trasladó a la organización <code>data-privacy-stack</code>\ny <code>github.com/microsoft/presidio</code> ahora responde <code>Moved Permanently</code>. Mismo proyecto, nuevo hogar, que es por qué las imágenes a continuación no están bajo\nuna ruta de Microsoft.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"agregar-presidio-al-compose-de-la-puerta-de-enlace\">Agregar Presidio al compose de la puerta de enlace<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#agregar-presidio-al-compose-de-la-puerta-de-enlace\" class=\"hash-link\" aria-label=\"Enlace directo al Agregar Presidio al compose de la puerta de enlace\" title=\"Enlace directo al Agregar Presidio al compose de la puerta de enlace\" translate=\"no\">​</a></h2>\n<p>Presidio solo habla con la puerta de enlace, por lo que no necesita puertos publicados.\nAbre el <code>docker-compose.yml</code> de la Parte 1 y agrega dos servicios después del bloque <code>db:</code>\n, encima de <code>volumes:</code>:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">docker-compose.yml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">presidio-analyzer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> ghcr.io/data</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">privacy</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">stack/presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">analyzer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">latest</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">container_name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">analyzer</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">restart</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> unless</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">stopped</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">presidio-anonymizer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> ghcr.io/data</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">privacy</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">stack/presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">anonymizer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">latest</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">container_name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">anonymizer</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">restart</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> unless</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">stopped</span><br></div></code></pre></div></div>\n<p>Dos espacios de sangría, al mismo nivel que <code>litellm:</code> y <code>db:</code>.</p>\n<p>Nota lo que está ausente: no hay <code>ports:</code>. Colocarlos en el mismo proyecto de compose que\nla puerta de enlace significa que Docker les da una red y DNS compartidos, por lo que la puerta de enlace\nlos alcanza por nombre de servicio y nada más en tu red puede alcanzarlos. Si desplegaste Presidio por separado con <code>ports: [\"5002:3000\"]</code>, eso\nexpondría tu analizador de PII a todo lo que pueda enrutar hacia el host — vale la pena deshacerlo.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/llm-gateway </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><br></div></code></pre></div></div>\n<p>Cuatro contenedores, y las filas de Presidio deberían mostrar <code>3000/tcp</code> sin mapeo de host:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">NAME                  SERVICE               STATUS         PORTS</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">llm-gateway           litellm               Up             127.0.0.1:4000-&gt;4000/tcp</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">llm-gateway-db        db                    Up (healthy)   5432/tcp</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">presidio-analyzer     presidio-analyzer     Up (healthy)   3000/tcp</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">presidio-anonymizer   presidio-anonymizer   Up (healthy)   3000/tcp</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Cuatro contenedores en ejecución, con ambos servicios de Presidio mostrando 3000/tcp y sin mapeo de puerto de host\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw3-compose-ps-57ff2d738b0cb45457ecc59f8325613d.png\" width=\"1732\" height=\"340\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Si <code>docker compose ps</code> muestra solo dos contenedores, el archivo de compose no se guardó.\nConfirma lo que compose está leyendo realmente:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose config </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--services</span><br></div></code></pre></div></div>\n<p>Eso debe listar cuatro nombres. Lee el archivo en disco, así que captura una edición que\nnunca salió de tu editor.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"verifica-el-analizador-y-aprende-sus-límites\">Verifica el analizador y aprende sus límites<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#verifica-el-analizador-y-aprende-sus-l%C3%ADmites\" class=\"hash-link\" aria-label=\"Enlace directo al Verifica el analizador y aprende sus límites\" title=\"Enlace directo al Verifica el analizador y aprende sus límites\" translate=\"no\">​</a></h2>\n<p>Antes de conectar nada, ve lo que Presidio realmente encuentra:</p>\n<p>No hay puertos de host ahora, así que pregunta desde dentro de la puerta de enlace. La imagen de LiteLLM\nno incluye <code>curl</code>, así que usa su Python:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> llm-gateway python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"import json,urllib.request as u; r=u.Request('http://presidio-analyzer:3000/analyze', data=json.dumps({'text':'Llama a Rafael al 555-0142 o rafael@example.com','language':'en'}).encode(), headers={'Content-Type':'application/json'}); print(u.urlopen(r).read().decode())\"</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"entity_type\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"EMAIL_ADDRESS\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"score\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1.0</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"start\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">27</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"end\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">45</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"entity_type\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"PERSON\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"score\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0.85</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"start\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">5</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"end\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">11</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"entity_type\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"URL\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"score\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0.5</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"start\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">34</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"end\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">45</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Respuesta del analizador puntuando el correo electrónico en 1.0 y el nombre en 0.85, sin número de teléfono detectado\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw3-analyze-scores-16b5f56ac49060ffafbba8dc8a2c2333.png\" width=\"1638\" height=\"488\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Lee eso cuidadosamente, porque establece expectativas para todo lo que sigue.</p>\n<p>Encontró el correo electrónico con plena confianza y el nombre en 0.85. <strong>No encontró el\nnúmero de teléfono en absoluto.</strong> <code>555-0142</code> son siete dígitos sin código de área, y\nPresidio valida contra planes de numeración reales en lugar de hacer coincidir patrones\nque se vean como teléfonos. Agrega un código de área:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> llm-gateway python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"import json,urllib.request as u; r=u.Request('http://presidio-analyzer:3000/analyze', data=json.dumps({'text':'Llama a Rafael al (415) 555-0142 o rafael@example.com','language':'en'}).encode(), headers={'Content-Type':'application/json'}); print(u.urlopen(r).read().decode())\"</span><br></div></code></pre></div></div>\n<p>Ahora aparece <code>PHONE_NUMBER</code> — puntuando <strong>0.4</strong>, frente a 1.0 para el correo electrónico.</p>\n<p>Vale la pena verificar si el prefijo <code>555</code> es el problema, ya que ese rango está\nreservado para ficción. No lo es. Un número plausible se comporta de manera idéntica:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> llm-gateway python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"import json,urllib.request as u; r=u.Request('http://presidio-analyzer:3000/analyze', data=json.dumps({'text':'Llama a Rafael al (415) 682-4531 o al 682-4531','language':'en'}).encode(), headers={'Content-Type':'application/json'}); print(u.urlopen(r).read().decode())\"</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"entity_type\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"PERSON\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"score\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0.85</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"start\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">5</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"end\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">11</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"entity_type\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"PHONE_NUMBER\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"score\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0.4</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"start\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">15</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"end\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">29</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><br></div></code></pre></div></div>\n<p>Misma puntuación de 0.4 para el número completo, y el bare <code>682-4531</code> sigue sin ser detectado. Así que dos\ncosas son ciertas: un código de área es lo que hace que un número de teléfono sea detectable,\ny <strong>0.4 es simplemente lo que este reconocedor devuelve para números de teléfono</strong> — no una\npenalización por números falsos.</p>\n<p>Lo que significa que los tipos de entidad no son igualmente confiables, y la confianza es un número\nque querrás establecer deliberadamente. Presidio expone <code>presidio_score_thresholds</code>\npara exactamente eso: un umbral por entidad por debajo del cual se descartan las detecciones. Establecerlo\nen 0.5 y dejarías de enmascarar silenciosamente cada número de teléfono en tu tráfico.</p>\n<p>No construyas una historia de cumplimiento sobre la suposición de que esto captura todo.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"enseñándole-los-números-que-se-pierde\">Enseñándole los números que se pierde<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#ense%C3%B1%C3%A1ndole-los-n%C3%BAmeros-que-se-pierde\" class=\"hash-link\" aria-label=\"Enlace directo al Enseñándole los números que se pierde\" title=\"Enlace directo al Enseñándole los números que se pierde\" translate=\"no\">​</a></h3>\n<p>Hay una salida, y vale la pena saberlo antes de decidir que la cobertura\nes inaceptable. <code>presidio_ad_hoc_recognizers</code> toma una ruta a un archivo JSON de\nreconocedores adicionales, cargados cuando se inicia la guardia:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">recognizers.json</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"name\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"LocalPhoneRecognizer\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"supported_language\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"en\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"supported_entity\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"PHONE_NUMBER\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"patterns\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"name\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"us-local-7\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"regex\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\\\\b\\\\d{3}-\\\\d{4}\\\\b\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"score\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0.9</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><br></div></code></pre></div></div>\n<p>Móntalo junto a la configuración y apunta la guardia hacia él:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">docker-compose.yml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> ./recognizers.json</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/app/recognizers.json</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">ro</span><br></div></code></pre></div></div>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">presidio_ad_hoc_recognizers</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> /app/recognizers.json</span><br></div></code></pre></div></div>\n<p>Recrea el contenedor — un nuevo volumen necesita <code>up -d</code>, no <code>restart</code> — y la misma\nsolicitud que se filtró ahora se enmascara:</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\">Respuesta OK. Contacta a &lt;PERSON&gt; al &lt;PHONE_NUMBER&gt; sobre la factura.</span><br></div></code></pre></div></div>\n<p><strong>Por qué la puntuación es 0.9 y no 0.8.</strong> Coloca <code>682-4531</code> dentro de una oración —\n<code>Llama a 682-4531</code> — y los reconocedores predeterminados devuelven <code>DATE_TIME</code> en\n<strong>0.85</strong>: siete dígitos con un guion, leídos como una fecha. (Pasa el número por sí solo,\nsin oración alrededor, y nada se activa; el modelo NER quiere\ncontexto antes de comprometerse a nada.) A 0.8 tu reconocedor de teléfono pierde la\nsuperposición y el número se enmascara como <code>&lt;DATE_TIME&gt;</code>: sigue redactado, pero archivado\nbajo la entidad equivocada, lo que rompe silenciosamente cualquier informe que cuente por tipo.\nA 0.86 o más <code>PHONE_NUMBER</code> gana.</p>\n<p>Medido en tres ejecuciones cada uno, completamente determinista:</p>\n<table><thead><tr><th>Texto</th><th>Predeterminado</th><th>Reconocedor a 0.8</th><th>Reconocedor a 0.9</th></tr></thead><tbody><tr><td><code>Llama a 555-0142</code></td><td><strong>dejado en claro</strong></td><td><code>&lt;PHONE_NUMBER&gt;</code></td><td><code>&lt;PHONE_NUMBER&gt;</code></td></tr><tr><td><code>Llama a 682-4531</code></td><td><code>&lt;DATE_TIME&gt;</code></td><td><code>&lt;DATE_TIME&gt;</code></td><td><code>&lt;PHONE_NUMBER&gt;</code></td></tr><tr><td><code>(415) 682-4531 o al 682-4531</code></td><td>uno bare <strong>filtra</strong></td><td>ambos enmascarados</td><td>ambos enmascarados</td></tr></tbody></table>\n<p>Nota que la fila del medio no es un fallo — es una clasificación errónea, y el anonimizador\naún lo redacta. Las primeras y terceras filas son las verdaderas filtraciones, y un reconocedor\ncierra ambas.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-versión-anterior-no-es-segura-para-enviar\">La versión anterior no es segura para enviar<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#la-versi%C3%B3n-anterior-no-es-segura-para-enviar\" class=\"hash-link\" aria-label=\"Enlace directo al La versión anterior no es segura para enviar\" title=\"Enlace directo al La versión anterior no es segura para enviar\" translate=\"no\">​</a></h3>\n<p>Siete dígitos y un guion son una forma, no un significado, y muchas cosas comparten eso.\nEjecuta ese reconocedor contra texto de una cola de soporte real:</p>\n<table><thead><tr><th>Texto</th><th>Enmascarado como</th></tr></thead><tbody><tr><td><code>El pedido 482-1099 se envió ayer</code></td><td><code>&lt;PHONE_NUMBER&gt;</code></td></tr><tr><td><code>La factura 100-2000 está vencida</code></td><td><code>&lt;PHONE_NUMBER&gt;</code></td></tr><tr><td><code>El número de parte 250-4000 está descontinuado</code></td><td><code>&lt;PHONE_NUMBER&gt;</code></td></tr><tr><td><code>El código de error era 500-1001</code></td><td><code>&lt;PHONE_NUMBER&gt;</code></td></tr><tr><td><code>Nuestra oficina está en 200-4500 Main Street</code></td><td><code>&lt;PHONE_NUMBER&gt;</code></td></tr><tr><td><code>RFC 793-1981 define TCP</code></td><td><code>&lt;PHONE_NUMBER&gt;</code></td></tr></tbody></table>\n<p>Seis de seis. Y el 0.9 que configuraste para ganar la superposición de <code>DATE_TIME</code> ahora gana <em>cada</em>\nsuperposición, por lo que el reconocedor no solo agrega ruido — sobrescribe clasificaciones correctas con una incorrecta. Rastreos llenos de <code>&lt;PHONE_NUMBER&gt;</code> donde solían estar los números de pedido son peores que rastreos con un número en claro, porque\nahora no puedes decir cuál es cuál.</p>\n<p>La solución es dejar de coincidir solo por forma y requerir una palabra clave delante de los\ndígitos. Eso tiene que ser un <strong>lookbehind</strong>: Presidio redacta toda la coincidencia, así que\n<code>(call\\W+)(\\d{3}-\\d{4})</code> tragaría la palabra \"llama\" junto con el número —\nlos grupos de captura no reducen el alcance. El <code>re</code> de Python solo permite lookbehinds de ancho fijo,\nque es por qué esto es una alternancia en lugar de una lista ordenada:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">recognizers.json</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"name\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"LocalPhoneRecognizer\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"supported_language\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"en\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"supported_entity\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"PHONE_NUMBER\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"patterns\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token property\" style=\"color:#36acaa\">\"name\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"us-local-7-cued\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token property\" style=\"color:#36acaa\">\"regex\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"(?i)(?:(?&lt;=call )|(?&lt;=called )|(?&lt;=calling )|(?&lt;=phone )|(?&lt;=phone: )|(?&lt;=phone is )|(?&lt;=tel )|(?&lt;=tel: )|(?&lt;=cell )|(?&lt;=cell: )|(?&lt;=cell is )|(?&lt;=mobile )|(?&lt;=mobile: )|(?&lt;=contact )|(?&lt;=contact: )|(?&lt;=number )|(?&lt;=number: )|(?&lt;=number is )|(?&lt;=me on )|(?&lt;=him on )|(?&lt;=her on )|(?&lt;=them on )|(?&lt;=us on )|(?&lt;=me at )|(?&lt;=him at )|(?&lt;=her at )|(?&lt;=them at )|(?&lt;=us at ))(?&lt;!part number )(?&lt;!serial number )(?&lt;!order number )(?&lt;!invoice number )(?&lt;!account number )(?&lt;!model number )(?&lt;!tracking number )(?&lt;!reference number )(?&lt;!batch number )(?&lt;!ticket number )(?&lt;!case number )\\\\d{3}-\\\\d{4}\\\\b\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token property\" style=\"color:#36acaa\">\"score\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0.9</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><br></div></code></pre></div></div>\n<p>Los lookbehinds negativos finales están ahí porque <code>number </code> es una pista útil y\n<code>part number </code> no lo es.</p>\n<p>Medido en doce frases de teléfono y doce similares:</p>\n<table><thead><tr><th></th><th><code>\\b\\d{3}-\\d{4}\\b</code></th><th>Anclado por pista</th></tr></thead><tbody><tr><td>Números de teléfono enmascarados</td><td>12/12</td><td>12/12</td></tr><tr><td>Similares enmascarados erróneamente</td><td><strong>12/12</strong></td><td><strong>0/12</strong></td></tr></tbody></table>\n<p>Dos cosas que esto no soluciona, y debes conocer ambas. <code>El pedido 482-1099</code> sigue\nregresando como <code>&lt;DATE_TIME&gt;</code> — eso es un fallo de spaCy por sí mismo, con o sin\ntu reconocedor. Y un número de teléfono introducido por una frase que no pensaste\nvuelve a filtrarse. Una lista de pistas es una suposición sobre cómo la gente escribe, así que trátala\ncomo algo que revisar contra tu propio tráfico en lugar de un artefacto terminado.</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>Números brasileños fallan de la misma manera</div><div class=\"admonitionContent_BuS1\"><p><code>BR</code> está en la lista de regiones predeterminadas, así que <code>(11) 91234-5678</code> es reconocido — y\n<code>91234-5678</code> sin el DDD no lo es, exactamente como con un código de área de EE. UU. faltante. El\nmismo reconocedor lo cierra con <code>\\d{4,5}-\\d{4}</code> y una lista de pistas en portugués.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"conectar-la-guardia\">Conectar la guardia<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#conectar-la-guardia\" class=\"hash-link\" aria-label=\"Enlace directo al Conectar la guardia\" title=\"Enlace directo al Conectar la guardia\" translate=\"no\">​</a></h2>\n<p>La puerta de enlace llama a Presidio a través de una guardia. Abre <code>config.yaml</code> y agrega un\nbloque <code>guardrails:</code> al final — esta es la configuración que la documentación de LiteLLM\nda para enmascarar solo en el camino de registro:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">guardrails</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">guardrail_name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">log</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">mask</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">litellm_params</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">guardrail</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> presidio</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">mode</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> logging_only</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">default_on</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean important\" style=\"color:#36acaa\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">presidio_analyzer_api_base</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> http</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">//presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">analyzer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">3000</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">presidio_anonymizer_api_base</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> http</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">//presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">anonymizer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">3000</span><br></div></code></pre></div></div>\n<p><code>mode: logging_only</code> es el importante. La documentación lo describe como:</p>\n<blockquote>\n<p>Ejecutar después de la llamada LLM, aplicar solo el enmascaramiento de PII antes de registrar en Langfuse, etc.\nNo en la solicitud / respuesta real de la API llm.</p>\n</blockquote>\n<p><code>default_on: true</code> lo aplica a cada solicitud en lugar de solo a aquellas que lo piden\npor nombre. Y los dos valores <code>api_base</code> son los nombres de servicio del archivo\ncompose — la guardia se ejecuta dentro del contenedor de la puerta de enlace, así que <code>localhost</code>\napuntaría al lugar equivocado.</p>\n<p>Reinicia y espera a que el proxy realmente regrese — toma de 30 a 60\nsegundos, que es más largo que la mayoría de los comandos <code>sleep</code> que la gente pone delante de él:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose restart litellm</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">until</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sf</span><span class=\"token plain\"> localhost:4000/health/readiness </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">printf</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.'</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">3</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">done</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\" listo\"</span><br></div></code></pre></div></div>\n<p>Luego confirma que la guardia se cargó, lo cual es más confiable que leer registros:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">grep</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-m1</span><span class=\"token variable\" style=\"color:#36acaa\"> LITELLM_MASTER_KEY ~/llm-gateway/.env </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">cut</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token variable operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\"> -f2- </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">tr</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'\"'</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> localhost:4000/guardrails/list </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"import sys,json; g=json.load(sys.stdin)['guardrails'][0]; p=g['litellm_params']; print(json.dumps({'guardrail_name': g['guardrail_name'], **{k: p.get(k) for k in ('guardrail','mode','presidio_filter_scope','default_on','presidio_analyzer_api_base','presidio_anonymizer_api_base','fail_on_error','unreachable_fallback')}}, indent=2))\"</span><br></div></code></pre></div></div>\n<p>El punto final devuelve cada posible campo de guardia, la mayoría de ellos <code>null</code>, así que esto\nselecciona los que importan:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"guardrail_name\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"presidio-log-mask\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"guardrail\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"presidio\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"mode\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"logging_only\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"presidio_filter_scope\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"input\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"default_on\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">true</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"presidio_analyzer_api_base\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://presidio-analyzer:3000\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"presidio_anonymizer_api_base\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://presidio-anonymizer:3000\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"fail_on_error\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">true</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"unreachable_fallback\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"fail_closed\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>Esos últimos dos son los valores predeterminados que vale la pena conocer antes de que esto vea tráfico real: si\nPresidio no está disponible, las solicitudes fallan en lugar de pasar sin enmascarar.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El punto final de la lista de guardias informa que presidio-log-mask se cargó, con modo logging_only y ambas bases de API de Presidio resueltas\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw3-guardrail-list-cd2a45b09b50cc051970b5b85feba496.png\" width=\"1632\" height=\"732\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"pruébalo-y-obtén-una-sorpresa\">Pruébalo y obtén una sorpresa<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#pru%C3%A9balo-y-obt%C3%A9n-una-sorpresa\" class=\"hash-link\" aria-label=\"Enlace directo al Pruébalo y obtén una sorpresa\" title=\"Enlace directo al Pruébalo y obtén una sorpresa\" translate=\"no\">​</a></h2>\n<p>Ahora la parte que importa. Envía una solicitud cuya <em>respuesta</em> pruebe si el modelo\nvio el texto real — pedirle que repita la PII de vuelta es inútil, porque la respuesta\ntambién se escanea:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">grep</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-m1</span><span class=\"token variable\" style=\"color:#36acaa\"> LITELLM_MASTER_KEY ~/llm-gateway/.env </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">cut</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token variable operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\"> -f2- </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">tr</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'\"'</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> localhost:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-small\",\"temperature\":0,\"messages\":[{\"role\":\"user\",\"content\":\"¿A qué ciudad de EE. UU. pertenece este código de área: (415) 555-0142? Responde solo con el nombre de la ciudad.\"}]}'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"import sys,json; print(json.load(sys.stdin)['choices'][0]['message']['content'])\"</span><br></div></code></pre></div></div>\n<p>Esperado: <code>San Francisco</code>, probando que el modelo leyó los dígitos.</p>\n<p>Real:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">&lt;LOCATION&gt;</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El modelo respondiendo a una pregunta que requiere leer el código de área, y la respuesta devuelta como el marcador de posición LOCATION\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw3-masked-answer-1467b181d83013e63ad42b11ffcdfa95.png\" width=\"1416\" height=\"248\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>El modelo respondió a una pregunta que requiere leer un código de área real — y luego\nla respuesta misma regresó enmascarada. <code>logging_only</code> se suponía que debía permanecer fuera de\nla respuesta real.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"por-qué-sucede-eso\">Por qué sucede eso<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#por-qu%C3%A9-sucede-eso\" class=\"hash-link\" aria-label=\"Enlace directo al Por qué sucede eso\" title=\"Enlace directo al Por qué sucede eso\" translate=\"no\">​</a></h2>\n<p>Hay una segunda configuración, y su valor predeterminado está haciendo el trabajo.</p>\n<p><code>presidio_filter_scope</code> controla qué dirección se escanea. De la\ndocumentación:</p>\n<blockquote>\n<p><code>input</code>: solo se escanea el contenido de usuario → modelo; <code>output</code>: solo se escanea el contenido de modelo → usuario; <code>both</code> (predeterminado): escanear ambas direcciones</p>\n</blockquote>\n<p>Y, en la misma página:</p>\n<blockquote>\n<p>Usa <code>presidio_filter_scope: output</code> (o <code>both</code>) cuando quieras que Presidio escanee y enmascare activamente la respuesta del modelo antes de que llegue al usuario.</p>\n</blockquote>\n<p>Así que <code>both</code> — el predeterminado — enmascara activamente las respuestas. El ejemplo documentado de <code>logging_only</code>\nno establece <code>presidio_filter_scope</code> en absoluto, lo que lo deja en <code>both</code>.\nEl resultado es una configuración que enmascara la cosa que la misma página dice que no\ntocará.</p>\n<p>En el código fuente, <code>initialize_presidio</code> lee ese alcance y registra un segundo\ncallback con <code>apply_to_output=True</code> en <code>post_call</code> siempre que el escaneo de salida esté\nhabilitado. El gancho de evento de ese callback está codificado, por lo que el <code>mode</code> que estableces nunca\nllega a él.</p>\n<p>Esto ha sido reportado.\n<a href=\"https://github.com/BerriAI/litellm/issues/30447\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Issue #30447</a>, <em>\"la guardia de Presidio en modo logging_only corrompe la respuesta orientada al usuario (contenido del asistente reemplazado con tokens de PII)\"</em>, fue presentado el 15 de junio de 2026 y cerrado al día siguiente sin discusión, junto con una solicitud de extracción titulada <em>\"fix(presidio): no enmascarar la solicitud en vivo cuando la guardia es logging_only\"</em>. El informe trataba sobre la respuesta; la solución abordó la solicitud. En la imagen <code>main-stable</code> utilizada aquí, el comportamiento anterior aún se reproduce.</p>\n<p>Un segundo informe está abierto:\n<a href=\"https://github.com/BerriAI/litellm/issues/35951\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">#35951</a> señala que en\nmodo <code>logging_only</code> el gancho de enmascaramiento reescribe <code>kwargs[\"messages\"]</code> pero no\npropaga a <code>standard_logging_object</code>, que es lo que leen los registradores posteriores.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-solución\">La solución<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#la-soluci%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al La solución\" title=\"Enlace directo al La solución\" translate=\"no\">​</a></h2>\n<p>Una línea. Agrega <code>presidio_filter_scope: input</code> a la guardia:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">guardrails</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">guardrail_name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">log</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">mask</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">litellm_params</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">guardrail</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> presidio</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">mode</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> logging_only</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">presidio_filter_scope</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> input</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">default_on</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean important\" style=\"color:#36acaa\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">presidio_analyzer_api_base</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> http</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">//presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">analyzer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">3000</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">presidio_anonymizer_api_base</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> http</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">//presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">anonymizer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">3000</span><br></div></code></pre></div></div>\n<p>Reinicia, espera a que esté listo y ejecuta la misma solicitud:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">San Francisco</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes ejecutar Presidio junto a una puerta de enlace, enmascarar solicitudes en el momento del registro sin\ntocar la solicitud en vivo, y distinguir las dos direcciones con\n<code>presidio_filter_scope</code>.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"demostrando-correctamente\">Demostrando correctamente<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#demostrando-correctamente\" class=\"hash-link\" aria-label=\"Enlace directo al Demostrando correctamente\" title=\"Enlace directo al Demostrando correctamente\" translate=\"no\">​</a></h2>\n<p>Una respuesta correcta podría ser una suposición afortunada — \"San Francisco\" es un predeterminado plausible\npara una pregunta de código de área enmascarado, y <code>temperature: 0</code> hace que una suposición se repita\ntan confiablemente como una respuesta real. Así que pregunta sobre varios códigos de área que el modelo no\npuede engañar:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">grep</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-m1</span><span class=\"token variable\" style=\"color:#36acaa\"> LITELLM_MASTER_KEY ~/llm-gateway/.env </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">cut</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token variable operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\"> -f2- </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">tr</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'\"'</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> </span><span class=\"token for-or-select variable\" style=\"color:#36acaa\">ac</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">907</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">808</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">216</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">505</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token builtin class-name\">printf</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"área %s -&gt; \"</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$ac</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> localhost:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"{</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">model</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">qwen-mid</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">,</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">temperature</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:0,</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">messages</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:[{</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">role</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">user</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">,</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">content</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">¿A qué estado de EE. UU. pertenece el código de área de este número de teléfono: (</span><span class=\"token string variable\" style=\"color:#36acaa\">$ac</span><span class=\"token string\" style=\"color:#e3116c\">) 555-0142? Responde solo con el nombre del estado.</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">}]}\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"import sys,json; print(' '.join(json.load(sys.stdin)['choices'][0]['message']['content'].split()))\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">done</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">área 907 -&gt; Alaska</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">área 808 -&gt; Hawaii</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">área 216 -&gt; Ohio</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">área 505 -&gt; Nuevo México</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Cuatro códigos de área diferentes cada uno devolviendo el estado correcto, probando que la solicitud llegó al modelo sin enmascarar\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw3-area-codes-9448b7ab1d15d52e9cafb0d8ad0e5b86.png\" width=\"1574\" height=\"356\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Cuatro de cuatro. El modelo está leyendo los dígitos, así que la solicitud le está llegando\nsin enmascarar. La colapsación de espacios en blanco en ese comando no es una tontería cosmética — <code>qwen-mid</code> es\nun modelo de razonamiento y antepone sus respuestas con saltos de línea, lo suficientemente inconsistentes\nque <code>.strip()</code> solo deja una salida irregular.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"por-qué-no-simplemente-enmascarar-antes-del-modelo\">¿Por qué no simplemente enmascarar antes del modelo?<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#por-qu%C3%A9-no-simplemente-enmascarar-antes-del-modelo\" class=\"hash-link\" aria-label=\"Enlace directo al ¿Por qué no simplemente enmascarar antes del modelo?\" title=\"Enlace directo al ¿Por qué no simplemente enmascarar antes del modelo?\" translate=\"no\">​</a></h2>\n<p>Es la pregunta obvia, y la respuesta obvia es incorrecta. <code>mode: pre_call</code>\nenmascara la solicitud antes de que el modelo la vea — lo que suena como el arreglo más seguro posible.</p>\n<p>Cambia la única línea y descubre:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">mode</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> pre_call</span><br></div></code></pre></div></div>\n<p>Reinicia, luego pregunta la misma cosa:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">grep</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-m1</span><span class=\"token variable\" style=\"color:#36acaa\"> LITELLM_MASTER_KEY ~/llm-gateway/.env </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">cut</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token variable operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\"> -f2- </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">tr</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'\"'</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> localhost:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-small\",\"temperature\":0,\"max_tokens\":60,\"messages\":[{\"role\":\"user\",\"content\":\"¿A qué estado pertenece el código de área de este número de teléfono: (907) 555-0142? Responde solo con el nombre del estado.\"}]}'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"import sys,json; print(' '.join(json.load(sys.stdin)['choices'][0]['message']['content'].split()))\"</span><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\">Para determinar el estado de un código de área de un número de teléfono dado, necesitaría saber</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">el código de área específico del número de teléfono proporcionado. Por favor, proporciona el código de área</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">o el número de teléfono completo para que pueda identificar el estado correspondiente.</span><br></div></code></pre></div></div>\n<p>La redacción exacta se mueve entre ejecuciones incluso a <code>temperature: 0</code> — el punto\nes que cada versión de ella te pide el número que ya enviaste.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Bajo el enmascaramiento pre_call, el modelo pide el número de teléfono que ya se le dio, habiendo recibido\nsolo un marcador de posición\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw3-precall-blind-12b8776adeda133b79cac28a0bb36166.png\" width=\"1564\" height=\"308\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>El modelo está pidiendo educadamente el número que acabas de darle. Recibió\n<code>&lt;PHONE_NUMBER&gt;</code> y no hay nada en un marcador de posición para razonar. Nada falló, nada\ndio error — simplemente obtuviste una respuesta inútil, que es el modo de fallo que es más difícil de notar en producción.</p>\n<p><code>qwen-mid</code> se comportó peor en mis ejecuciones: la misma solicitud se agotó dos veces a 40 segundos con <code>max_tokens: 60</code>, donde respondió en menos de un segundo con entrada sin enmascarar. No reclamaría un mecanismo de dos muestras, pero si estás\ndirigiéndote a un modelo de razonamiento, mide esto antes de confiar en él.</p>\n<p>Así que las tres ubicaciones, la misma pregunta cada vez:</p>\n<table><thead><tr><th>Configuración</th><th>El modelo recibe</th><th>Resultado</th></tr></thead><tbody><tr><td><code>mode: pre_call</code></td><td><code>&lt;PHONE_NUMBER&gt;</code></td><td>te pide el número que se le dio</td></tr><tr><td><code>mode: logging_only</code>, alcance dejado por defecto</td><td>dígitos reales</td><td>respuesta correcta, devuelta como <code>&lt;LOCATION&gt;</code></td></tr><tr><td><code>mode: logging_only</code>, <code>presidio_filter_scope: input</code></td><td>dígitos reales</td><td><code>San Francisco</code></td></tr></tbody></table>\n<p>Solo el tercero te da una aplicación funcional. Pon <code>mode</code> de vuelta a\n<code>logging_only</code> antes de continuar.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-esto-te-da-y-lo-que-no\">Lo que esto te da, y lo que no<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#lo-que-esto-te-da-y-lo-que-no\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que esto te da, y lo que no\" title=\"Enlace directo al Lo que esto te da, y lo que no\" translate=\"no\">​</a></h2>\n<p>Ahora tienes solicitudes que llegan a los modelos intactas, con el enmascaramiento de PII adjunto al\ncamino de registro en lugar del camino de solicitud — y a una configuración de distancia de enmascarar silenciosamente\nlas respuestas de tus usuarios.</p>\n<p>Una cosa que esta solución no cubre, y es importante: el alcance <code>input</code> enmascara solo la solicitud.\nLo que sea que el modelo <em>diga</em> se registra tal cual — y un modelo preguntado sobre un cliente repetirá el nombre de ese cliente. Así que la PII que eliminaste\nde la solicitud reaparece en la finalización. Ampliar el alcance a <code>both</code> cierra\nese agujero y reintroduce la corrupción de respuesta anterior; no hay configuración que haga ambas.\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/\">La Parte 4</a> mide la filtración\ny construye la pieza que falta en <code>async_logging_hook</code>, el callback documentado\nque se ejecuta después de que se ha enviado la respuesta.</p>\n<p>Siendo directo sobre el límite: la copia enmascarada aterriza donde sea que tus callbacks de registro la envíen. La propia tabla <code>LiteLLM_SpendLogs</code> de la puerta de enlace no es una de\nesas — <code>messages</code> se mantuvo en <code>{}</code> en cada configuración que probé, incluso con\n<code>store_prompts_in_spend_logs</code> configurado tanto en <code>config.yaml</code> como como variable de entorno. La documentación apunta a Langfuse y destinos similares, que es\ndonde se ve la próxima publicación. <strong>Hasta que hayas conectado uno y visto una carga enmascarada en él, trata la mitad de enmascaramiento como no probada en tu propio despliegue.</strong></p>\n<p>Dos cosas más que vale la pena saber antes de que esto se ponga frente a tráfico real. La\nguardia predetermina <code>fail_on_error: true</code> con <code>unreachable_fallback: fail_closed</code>, así que si Presidio se cae tus solicitudes fallan en lugar de pasar\nsin enmascarar — el valor predeterminado correcto, pero planifica para ello. Y cada llamada a Presidio agrega\nlatencia al camino de registro; el método de temporización de la Parte 2 se aplica aquí también.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-de-problemas\">Solución de problemas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#soluci%C3%B3n-de-problemas\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas\" title=\"Enlace directo al Solución de problemas\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"docker-compose-ps-muestra-dos-contenedores-no-cuatro\"><code>docker compose ps</code> muestra dos contenedores, no cuatro<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#docker-compose-ps-muestra-dos-contenedores-no-cuatro\" class=\"hash-link\" aria-label=\"Enlace directo al docker-compose-ps-muestra-dos-contenedores-no-cuatro\" title=\"Enlace directo al docker-compose-ps-muestra-dos-contenedores-no-cuatro\" translate=\"no\">​</a></h3>\n<p>El archivo de compose no se guardó. <code>docker compose config --services</code> lee lo que está\nen disco — si Presidio falta allí, la edición no se realizó.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"curl-archivo-ejecutable-no-encontrado-en-path\"><code>curl: archivo ejecutable no encontrado en $PATH</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#curl-archivo-ejecutable-no-encontrado-en-path\" class=\"hash-link\" aria-label=\"Enlace directo al curl-archivo-ejecutable-no-encontrado-en-path\" title=\"Enlace directo al curl-archivo-ejecutable-no-encontrado-en-path\" translate=\"no\">​</a></h3>\n<p>La imagen de LiteLLM no tiene <code>curl</code>. Sondea con la línea de un solo comando <code>python -c</code> anterior.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"nombre-o-servicio-no-conocido-al-alcanzar-presidio-analyzer\"><code>Nombre o servicio no conocido</code> al alcanzar <code>presidio-analyzer</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#nombre-o-servicio-no-conocido-al-alcanzar-presidio-analyzer\" class=\"hash-link\" aria-label=\"Enlace directo al nombre-o-servicio-no-conocido-al-alcanzar-presidio-analyzer\" title=\"Enlace directo al nombre-o-servicio-no-conocido-al-alcanzar-presidio-analyzer\" translate=\"no\">​</a></h3>\n<p>Las dos pilas están en diferentes redes de Docker. Ambos conjuntos de servicios deben estar en\nel mismo proyecto de compose, o compartir una red externa.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-inicio-falla-con-validationerror-mode-field-required\">El inicio falla con <code>ValidationError: mode Field required</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#el-inicio-falla-con-validationerror-mode-field-required\" class=\"hash-link\" aria-label=\"Enlace directo al el-inicio-falla-con-validationerror-mode-field-required\" title=\"Enlace directo al el-inicio-falla-con-validationerror-mode-field-required\" translate=\"no\">​</a></h3>\n<p><code>mode</code> es obligatorio en una guardia. Omitirlo sale del proxy al inicio —\n<code>Application startup failed. Exiting.</code> No puedes expresar solo registro dejando\nfuera <code>mode</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-modelo-responde-correctamente-pero-la-respuesta-es-location-o-phone_number\">El modelo responde correctamente pero la respuesta es <code>&lt;LOCATION&gt;</code> o <code>&lt;PHONE_NUMBER&gt;</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#el-modelo-responde-correctamente-pero-la-respuesta-es-location-o-phone_number\" class=\"hash-link\" aria-label=\"Enlace directo al el-modelo-responde-correctamente-pero-la-respuesta-es-location-o-phone_number\" title=\"Enlace directo al el-modelo-responde-correctamente-pero-la-respuesta-es-location-o-phone_number\" translate=\"no\">​</a></h3>\n<p><code>presidio_filter_scope</code> está en su predeterminado <code>both</code>. Establecerlo en <code>input</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"un-número-de-teléfono-no-se-está-enmascarando\">Un número de teléfono no se está enmascarando<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#un-n%C3%BAmero-de-tel%C3%A9fono-no-se-est%C3%A1-enmascarando\" class=\"hash-link\" aria-label=\"Enlace directo al Un número de teléfono no se está enmascarando\" title=\"Enlace directo al Un número de teléfono no se está enmascarando\" translate=\"no\">​</a></h3>\n<p>Verifica la puntuación. <code>555-0142</code> no se detecta en absoluto; <code>(415) 555-0142</code> puntúa 0.4.\nUsa <code>presidio_score_thresholds</code> para establecer un umbral por entidad.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"los-cambios-en-configyaml-parecen-no-tener-efecto\">Los cambios en <code>config.yaml</code> parecen no tener efecto<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#los-cambios-en-configyaml-parecen-no-tener-efecto\" class=\"hash-link\" aria-label=\"Enlace directo al los-cambios-en-configyaml-parecen-no-tener-efecto\" title=\"Enlace directo al los-cambios-en-configyaml-parecen-no-tener-efecto\" translate=\"no\">​</a></h3>\n<p>Edita el archivo, <em>luego</em> reinicia — en ese orden. Y un <code>cd</code> de un paso anterior\nse lleva consigo, así que verifica en qué directorio estás antes de ejecutar <code>docker compose</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"los-cambios-en-env-son-ignorados\">Los cambios en <code>.env</code> son ignorados<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#los-cambios-en-env-son-ignorados\" class=\"hash-link\" aria-label=\"Enlace directo al los-cambios-en-env-son-ignorados\" title=\"Enlace directo al los-cambios-en-env-son-ignorados\" translate=\"no\">​</a></h3>\n<p><code>docker compose restart</code> no recarga el entorno. Usa\n<code>docker compose up -d --force-recreate litellm</code>.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Mask PII at the gateway</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-sigue\">Qué sigue<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al Qué sigue\" title=\"Enlace directo al Qué sigue\" translate=\"no\">​</a></h2>\n<p>El enmascaramiento está configurado, pero aún no has visto aterrizar una carga enmascarada\nen ninguna parte. La próxima publicación conecta un destino de registro real a la puerta de enlace y busca\n<code>&lt;PERSON&gt;</code> en un rastreo — cerrando el bucle que esta deja deliberadamente abierto —\ny luego se dirige a presupuestos y lo que realmente cuesta todo el arreglo.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lectura-adicional\">Lectura adicional<a href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/#lectura-adicional\" class=\"hash-link\" aria-label=\"Enlace directo al Lectura adicional\" title=\"Enlace directo al Lectura adicional\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.litellm.ai/docs/proxy/guardrails/pii_masking_v2\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Enmascaramiento de PII, PHI — Presidio</a> — la referencia de guardia, incluyendo <code>presidio_filter_scope</code> y <code>presidio_score_thresholds</code></li>\n<li class=\"\"><a href=\"https://docs.litellm.ai/docs/proxy/logging\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Registro, Alertas, Métricas</a> — los destinos a los que puede ir una copia enmascarada</li>\n<li class=\"\"><a href=\"https://github.com/data-privacy-stack/presidio\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Presidio</a> — el analizador y el anonimizador</li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/news/mask-your-logs-not-your-prompts/\">Enmascara tus registros, no tus solicitudes</a> — por qué el enmascaramiento pertenece al camino de almacenamiento</li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/\">Parte 1 — desplegar una puerta de enlace LLM en una instancia WEC</a></li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/\">Parte 2 — enrutamiento de complejidad y lo que cuesta</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/",
            "title": "Enmascarar PII en la puerta de enlace: configurar Presidio, además de la línea que los documentos omiten",
            "summary": "Configura el enmascaramiento de PII en una puerta de enlace LLM autoalojada con Presidio. El enmascaramiento puede estar en tres lugares: antes del modelo, en la respuesta, o solo en el camino hacia tus registros, y solo uno mantiene tu aplicación funcionando. Sigue la configuración documentada y las respuestas de tu modelo regresan redactadas; aquí está el porqué y la única configuración que lo soluciona.",
            "date_modified": "2026-08-20T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "llm",
                "gateway",
                "pii",
                "privacy",
                "guardrails",
                "autoalojamiento",
                "litellm",
                "presidio",
                "wec"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/litellm-wordmark-86d6f0432720b27534e05bc579dfc29c.png\" alt=\"LiteLLM\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/qwen-209001e8f781e3651b8d7b863343f2dc.png\" alt=\"Qwen\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 5 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->5</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->5<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting an LLM gateway</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">One endpoint, scoped keys</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">2</span><span class=\"skillTracker__skill\" data-state=\"current\">Route work to the right model</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Mask PII at the gateway</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Clean traces, untouched answers</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/load-test-llm-gateway-blocking-callback/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove it holds under load</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/\">Parte 1</a> terminó en un número incómodo. La misma respuesta de tres palabras costó 3 tokens de un modelo pequeño y 200 de un modelo de razonamiento — que gastó los 200 pensando y no devolvió nada en absoluto.</p>\n<p>Cada solicitud que envían tus aplicaciones elige un modelo, y en su mayoría esa elección se hace una vez, codificada de forma rígida, y nunca se revisita. Esta publicación pone al gateway a cargo de ello en su lugar: clasifica la solicitud, la enruta a un modelo adecuado para el trabajo. Luego mide lo que cuesta esa decisión, porque no es gratis y la mayoría de los informes omiten esa parte.</p>\n<!-- -->\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"agregando-el-enrutador-de-complejidad-de-litellm\">Agregando el enrutador de complejidad de LiteLLM<a href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/#agregando-el-enrutador-de-complejidad-de-litellm\" class=\"hash-link\" aria-label=\"Enlace directo al Agregando el enrutador de complejidad de LiteLLM\" title=\"Enlace directo al Agregando el enrutador de complejidad de LiteLLM\" translate=\"no\">​</a></h2>\n<p>El gateway de la Parte 1 ya tiene tres modelos registrados. Un enrutador es solo otra entrada en <code>model_list</code> que asigna niveles de complejidad a ellos:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">model_name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> smart</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">router</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">litellm_params</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">model</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> auto_router/complexity_router</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">complexity_router_config</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">tiers</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">SIMPLE</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> qwen</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">small</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">MEDIUM</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> qwen</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">mid</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">COMPLEX</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> qwen</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">large</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">REASONING</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> qwen</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">large</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">return_raw_model_name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean important\" style=\"color:#36acaa\">true</span><br></div></code></pre></div></div>\n<p><code>return_raw_model_name: true</code> es el importante por ahora. Sin él, la respuesta informa <code>smart-router</code> y no tienes idea de qué modelo te sirvió. Con él, la respuesta nombra el modelo que realmente se ejecutó — así que cada prueba a continuación se verifica por sí misma.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose restart litellm</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"observando-cómo-enruta\">Observando cómo enruta<a href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/#observando-c%C3%B3mo-enruta\" class=\"hash-link\" aria-label=\"Enlace directo al Observando cómo enruta\" title=\"Enlace directo al Observando cómo enruta\" translate=\"no\">​</a></h2>\n<p>Enviarás la misma solicitud muchas veces con solo el prompt cambiando, así que envuélvelo una vez:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function-name function\" style=\"color:#d73a49\">ask</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$LITELLM_MASTER_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"{</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">model</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">smart-router</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">,</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">messages</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:[{</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">role</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">user</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">,</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">content</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$1</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">}],</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">max_tokens</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:10}\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> .model</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>Luego cinco prompts de dificultad creciente:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ask </span><span class=\"token string\" style=\"color:#e3116c\">\"hi\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ask </span><span class=\"token string\" style=\"color:#e3116c\">\"what is a vpc\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ask </span><span class=\"token string\" style=\"color:#e3116c\">\"write a python function that retries an http call with backoff\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ask </span><span class=\"token string\" style=\"color:#e3116c\">\"our two services deadlock under load, walk me through diagnosing it\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ask </span><span class=\"token string\" style=\"color:#e3116c\">\"prove that the halting problem is undecidable\"</span><br></div></code></pre></div></div>\n<p>Una línea por prompt, en el orden enviado:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen2.5-3B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen2.5-3B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen3.5-9B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen2.5-3B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen2.5-3B-Instruct</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Cinco prompts enrutados por el puntuador heurístico, cuatro aterrizando en el modelo de 3B\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw2-heuristic-route-af0d1b1a3a318287720fa1b18160e760.png\" width=\"1806\" height=\"450\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figura 1.</strong> La solicitud de código subió un nivel. Las dos preguntas más difíciles no.</p>\n<p>Un saludo en el modelo pequeño es correcto. Una solicitud de Python en el modelo medio es correcta. Pero un bloqueo de sistemas distribuidos y una de las preguntas más difíciles en ciencias de la computación aterrizaron en un modelo de 3 mil millones de parámetros.</p>\n<p>Para entender por qué, debes saber lo que realmente está haciendo el enrutador.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-es-una-heurística-y-cómo-litellm-puntúa-una\">Qué es una heurística y cómo LiteLLM puntúa una<a href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/#qu%C3%A9-es-una-heur%C3%ADstica-y-c%C3%B3mo-litellm-punt%C3%BAa-una\" class=\"hash-link\" aria-label=\"Enlace directo al Qué es una heurística y cómo LiteLLM puntúa una\" title=\"Enlace directo al Qué es una heurística y cómo LiteLLM puntúa una\" translate=\"no\">​</a></h2>\n<p>Por defecto, este enrutador no hace llamadas a la API. Puntúa el prompt localmente con coincidencias de patrones — eso es lo que \"heurística\" significa aquí: una regla de oro barata que aproxima un juicio sin hacerlo.</p>\n<p>Puntúa siete dimensiones, cada una produciendo un valor entre −1 y +1, luego multiplica cada una por un peso fijo y las suma:</p>\n<table><thead><tr><th>Dimensión</th><th>Peso</th><th>Se activa en</th></tr></thead><tbody><tr><td><code>codePresence</code></td><td>0.30</td><td><code>function</code>, <code>class</code>, <code>api</code>, <code>schema</code>, …</td></tr><tr><td><code>reasoningMarkers</code></td><td>0.25</td><td>\"paso a paso\", \"pensar a través\", \"analizar\"</td></tr><tr><td><code>technicalTerms</code></td><td>0.25</td><td>\"arquitectura\", \"distribuido\", \"cifrado\"</td></tr><tr><td><code>tokenCount</code></td><td>0.10</td><td><strong>−1.0</strong> bajo 15 tokens, <strong>+1.0</strong> sobre 400</td></tr><tr><td><code>simpleIndicators</code></td><td>0.05</td><td>\"qué es\", \"definir\", saludos — puntúa <strong>−1.0</strong></td></tr><tr><td><code>multiStepPatterns</code></td><td>0.03</td><td>\"primero… luego\", pasos numerados</td></tr><tr><td><code>questionComplexity</code></td><td>0.02</td><td>más de tres signos de interrogación</td></tr></tbody></table>\n<p>La suma ponderada se asigna a un nivel en tres límites: por debajo de <strong>0.15</strong> es SIMPLE, por debajo de <strong>0.35</strong> MEDIUM, por debajo de <strong>0.60</strong> COMPLEX, y por encima de eso REASONING. Todos esos valores están documentados y son configurables.</p>\n<p>Dos dimensiones pueden empujar la puntuación <em>hacia abajo</em>. Un prompt corto puntúa −1.0 en <code>tokenCount</code>. Un prompt que contiene \"qué es\" puntúa −1.0 en <code>simpleIndicators</code>.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-aritmética-en-un-prompt-real\">La aritmética, en un prompt real<a href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/#la-aritm%C3%A9tica-en-un-prompt-real\" class=\"hash-link\" aria-label=\"Enlace directo al La aritmética, en un prompt real\" title=\"Enlace directo al La aritmética, en un prompt real\" translate=\"no\">​</a></h2>\n<p>El enrutador registra su propio trabajo. Cada solicitud enrutada escribe una decisión en el registro de gastos:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/spend/logs </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$LITELLM_MASTER_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.[].metadata.routing_decision | select(.) | \"\\(.tier)  \\(.score)  -&gt; \\(.routed_model)  \\(.signals)\"'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">head</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-5</span><br></div></code></pre></div></div>\n<p>Más reciente primero, así que esto se lee de abajo hacia arriba en contra del orden en que se enviaron los prompts:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">SIMPLE  -0.1  -&gt; qwen-small  [\"corto (11 tokens)\"]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">SIMPLE  0  -&gt; qwen-small  null</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">MEDIUM  0.3  -&gt; qwen-mid  [\"código (función, python)\"]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">SIMPLE  -0.15000000000000002  -&gt; qwen-small  [\"corto (3 tokens)\",\"simple (qué es)\"]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">SIMPLE  -0.15000000000000002  -&gt; qwen-small  [\"corto (0 tokens)\",\"simple (hola)\"]</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Decisiones de enrutamiento que muestran nivel, puntuación y las señales detrás de cada una\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw2-decisions-c9f72192a937dfe8928c7a438f555144.png\" width=\"1788\" height=\"274\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figura 2.</strong> No es una caja negra: el enrutador informa la puntuación y las señales que se activaron.</p>\n<p>Toma el último. <code>prueba que el problema de parada es indecidible</code>:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">tokenCount        -1.0 × 0.10 = -0.10    11 tokens, bajo el umbral de 15</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">codePresence       0.0 × 0.30 =  0.00    sin palabras clave de código</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">reasoningMarkers   0.0 × 0.25 =  0.00    \"prueba\" no está en la lista de marcadores</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">technicalTerms     0.0 × 0.25 =  0.00</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">simpleIndicators   0.0 × 0.05 =  0.00</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">multiStepPatterns  0.0 × 0.03 =  0.00</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">questionComplexity 0.0 × 0.02 =  0.00</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                ------</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                 -0.10   por debajo de 0.15 → SIMPLE → modelo 3B</span><br></div></code></pre></div></div>\n<p>Cada término es cero excepto una penalización por ser corto. La pregunta se enruta hacia abajo <em>porque es breve</em>.</p>\n<p>Ahora el prompt de bloqueo, que es peor: puntuó <strong>0.00 sin señales en absoluto</strong>. No es una puntuación baja — nada coincidió. \"Nuestros dos servicios se bloquean bajo carga, guíame a través del diagnóstico\" no contiene ninguna palabra clave que el puntuador reconozca, por lo que cae en SIMPLE por defecto.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes leer una decisión de enrutamiento — nivel, puntuación y las señales que la produjeron — y reproducir la aritmética a mano a partir de los pesos de las dimensiones.</p></div></div>\n<p>El enrutador no está fallando. Está haciendo exactamente lo que dicen sus reglas. Las reglas simplemente no tienen forma de ver la dificultad que no esté escrita en el vocabulario que conoce.</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>Donde esto duele</div><div class=\"admonitionContent_BuS1\"><p>El modo de falla es sistemático, no aleatorio: <strong>los prompts cortos que necesitan un pensamiento profundo se enrutan hacia abajo</strong>. Esos son también los prompts donde un modelo incorrecto es más obvio para el usuario.</p></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"una-palabra-clave-nunca-coincide-con-su-propio-plural\">Una palabra clave nunca coincide con su propio plural<a href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/#una-palabra-clave-nunca-coincide-con-su-propio-plural\" class=\"hash-link\" aria-label=\"Enlace directo al Una palabra clave nunca coincide con su propio plural\" title=\"Enlace directo al Una palabra clave nunca coincide con su propio plural\" translate=\"no\">​</a></h3>\n<p>Las palabras clave de una sola palabra se coinciden en los límites de las palabras, por lo que <code>endpoint</code> no coincide con <code>endpoints</code>. Cada palabra clave de una sola palabra en las listas predeterminadas es singular, y ninguna de ellas coincide con un plural.</p>\n<p>Si eso cambia algo depende de cuán cerca esté la puntuación de un límite, porque las dimensiones son escalonadas en lugar de lineales — <code>technicalTerms</code> puntúa 0.5 en dos coincidencias y 1.0 en cuatro, por lo que perder una coincidencia a menudo no cambia nada. A veces cambia el modelo:</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\">Revisa nuestro api endpoint para problemas de autenticación y autorización → COMPLEJO +0.425</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Revisa nuestros api endpoints para problemas de autenticación y autorización → MEDIUM  +0.275</span><br></div></code></pre></div></div>\n<p>Una letra, un nivel. La dirección siempre es la misma: un plural puntúa más bajo o igual, nunca más alto, por lo que la deriva es hacia el modelo más barato.</p>\n<p>Los plurales que te importan se pueden agregar a la lista técnica con <code>custom_technical_keywords</code>. No hay equivalente para palabras clave de código — la única palanca es <code>code_keywords</code>, que reemplaza la lista incorporada en lugar de extenderla.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"preguntando-a-un-modelo-en-su-lugar\">Preguntando a un modelo en su lugar<a href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/#preguntando-a-un-modelo-en-su-lugar\" class=\"hash-link\" aria-label=\"Enlace directo al Preguntando a un modelo en su lugar\" title=\"Enlace directo al Preguntando a un modelo en su lugar\" translate=\"no\">​</a></h2>\n<p>La alternativa es gastar una llamada de modelo en la decisión. Cuatro líneas:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">classifier_type</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> llm</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">classifier_llm_config</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">model</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> qwen</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">small</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">timeout_ms</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">3000</span><br></div></code></pre></div></div>\n<p>Ahora el enrutador envía el prompt a <code>qwen-small</code> con un rubro y un esquema que obliga a devolver exactamente uno de <code>SIMPLE</code>, <code>MEDIUM</code>, <code>COMPLEX</code>, <code>REASONING</code>, y enruta según la respuesta. El clasificador aquí es el <em>modelo más barato</em> que tenemos — el mismo 3B que estaba respondiendo incorrectamente a las preguntas difíciles hace un momento. Resulta ser un mejor juez de dificultad de lo que es un respondedor de ella.</p>\n<p>Reinicia, luego vuelve a ejecutar los cinco prompts idénticos:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen2.5-3B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen3.5-9B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen3.5-122B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen3.5-122B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen3.5-122B</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Los mismos cinco prompts bajo el clasificador LLM, con los difíciles ahora en el modelo de 122B\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw2-llm-route-9a7f2aa9f50bca506a1786ae36cf8c3b.png\" width=\"1262\" height=\"370\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figura 3.</strong> Ambos fallos corregidos — y dos prompts subieron que, argumentablemente, no deberían haberlo hecho.</p>\n<table><thead><tr><th>Prompt</th><th>Heurística</th><th>Clasificador LLM</th></tr></thead><tbody><tr><td><code>hi</code></td><td>3B</td><td>3B</td></tr><tr><td><code>qué es un vpc</code></td><td>3B</td><td>9B</td></tr><tr><td><code>función de python … retroceso</code></td><td>9B</td><td>122B</td></tr><tr><td><code>bloqueo bajo carga</code></td><td>3B ✗</td><td>122B ✓</td></tr><tr><td><code>problema de parada</code></td><td>3B ✗</td><td>122B ✓</td></tr></tbody></table>\n<p>Los dos casos rotos están arreglados. Pero lee de nuevo las filas 2 y 3 — todo subió. \"¿Qué es un vpc?\" es una búsqueda fáctica que un modelo de 3B responde perfectamente, y ahora se ejecuta en el 9B. Has dejado de subestimar los prompts difíciles y comenzado a sobreestimar los fáciles. Si esa compensación vale la pena depende de tu mezcla de tráfico, y deberías medir la tuya en lugar de confiar en esta tabla.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-cuesta\">Lo que cuesta<a href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/#lo-que-cuesta\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que cuesta\" title=\"Enlace directo al Lo que cuesta\" translate=\"no\">​</a></h2>\n<p>Esta es la parte que se omite. Agrega un gemelo que omita el enrutador, luego mide ambos:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function-name function\" style=\"color:#d73a49\">direct</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$LITELLM_MASTER_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"{</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">model</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">qwen-small</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">,</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">messages</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:[{</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">role</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">user</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">,</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">content</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$1</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">}],</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">max_tokens</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:10}\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> .model</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">time</span><span class=\"token plain\"> ask </span><span class=\"token string\" style=\"color:#e3116c\">\"hi\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">time</span><span class=\"token plain\"> ask </span><span class=\"token string\" style=\"color:#e3116c\">\"hi\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">time</span><span class=\"token plain\"> direct </span><span class=\"token string\" style=\"color:#e3116c\">\"hi\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen2.5-3B-Instruct     real  0m2.474s     ← primera llamada después del reinicio</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen2.5-3B-Instruct     real  0m1.041s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">qwen-small              real  0m0.529s</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"salida de tiempo comparando una llamada enrutada contra una llamada directa\" src=\"https://development-wec.wiline.com/docs/es/assets/images/gw2-timing-ea97fdf0902b60a49ea16f3951a173c6.png\" width=\"1802\" height=\"722\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figura 4.</strong> Mismo prompt, mismo modelo lo responde, el doble del tiempo de reloj.</p>\n<p>El mismo modelo de 3B produjo ambas respuestas. La única diferencia es que una solicitud fue clasificada primero. Ahora descompónlo: el registro de gastos mide cada llamada por separado:</p>\n<table><thead><tr><th>Reloj</th><th>Llamada del clasificador</th><th>Llamada de servicio</th></tr></thead><tbody><tr><td>2.474 s (frío)</td><td>1449 ms</td><td>806 ms</td></tr><tr><td>1.041 s</td><td>549 ms</td><td>456 ms</td></tr><tr><td>0.529 s (directo)</td><td>—</td><td>495 ms</td></tr></tbody></table>\n<p>La aritmética cierra: 549 + 456 = 1005 ms contra 1.041 s medidos en la terminal.</p>\n<p>Tres cosas se desprenden:</p>\n<p><strong>El costo adicional no es constante.</strong> A través de las muestras del día, la llamada del clasificador varió de 476 ms a 1449 ms, y la primera solicitud después de un reinicio costó 1449 ms por sí sola. Es una llamada de inferencia completa y hereda lo que sea que esté haciendo el backend. Cualquier número único que cites para ello es el número que te tocó capturar.</p>\n<p><strong>El costo de tokens es fijo y mayor de lo que pensarías.</strong> Cada clasificación envió <strong>281–293 tokens de entrada</strong> — el rubro — y devolvió alrededor de 10. Enrutar <code>hi</code>, un prompt de un token, cuesta ~281 tokens de clasificación antes de que algo responda.</p>\n<p><strong>La llamada de servicio también se ralentiza.</strong> 456–806 ms cuando se enruta, contra 495 ms directos para la solicitud idéntica. El clasificador parece dejar al backend ocupado para la solicitud en cola detrás de él. Nunca verías esto solo con el tiempo de reloj.</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>Mide las partes, no el total</div><div class=\"admonitionContent_BuS1\"><p>El tiempo de reloj emparejado te dice <em>que</em> se volvió más lento, nunca <em>dónde</em>. Descompón en la llamada del clasificador y la llamada de servicio antes de que cites una cifra — el primer número honesto aquí fue casi el doble del que una sola ejecución de <code>time</code> sugirió.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-trampa-que-vale-la-pena-conocer\">La trampa que vale la pena conocer<a href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/#la-trampa-que-vale-la-pena-conocer\" class=\"hash-link\" aria-label=\"Enlace directo al La trampa que vale la pena conocer\" title=\"Enlace directo al La trampa que vale la pena conocer\" translate=\"no\">​</a></h2>\n<p>Si la llamada de clasificación se agota, devuelve la forma incorrecta o vuelve vacía, el enrutador vuelve a <strong>el puntuador heurístico</strong> — la cosa cuyas fallas acabas de pagar medio segundo para evitar. Silenciosamente.</p>\n<p>La documentación oficial nombra la salida: establece <code>classifier_fallback: default_model</code> y un tiempo de espera enruta a un modelo que elegiste deliberadamente, en lugar de al puntuador que envía preguntas difíciles a un modelo de 3B.</p>\n<p>También vale la pena verificar en lugar de asumir: la documentación da a <code>timeout_ms</code> un valor predeterminado de 2000, mientras que el paquete en nuestro contenedor tiene un valor predeterminado de 3000. Lee la versión que realmente instalaste.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"entonces-cuál\">Entonces, ¿cuál?<a href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/#entonces-cu%C3%A1l\" class=\"hash-link\" aria-label=\"Enlace directo al Entonces, ¿cuál?\" title=\"Enlace directo al Entonces, ¿cuál?\" translate=\"no\">​</a></h2>\n<table><thead><tr><th></th><th>Heurística</th><th>Clasificador LLM</th></tr></thead><tbody><tr><td>Latencia añadida</td><td>sub-milisegundo</td><td>476–1449 ms</td></tr><tr><td>Llamadas API adicionales</td><td>ninguna</td><td>una por solicitud</td></tr><tr><td>Tokens de entrada adicionales</td><td>ninguno</td><td>~280 por solicitud</td></tr><tr><td>Prompts difíciles pero cortos</td><td>enrutados hacia abajo</td><td>enrutados correctamente</td></tr><tr><td>Prompts fáciles</td><td>enrutados correctamente</td><td>enrutados hacia arriba</td></tr><tr><td>Falla por</td><td>estar confidentemente equivocado</td><td>agotarse, luego estar confidentemente equivocado</td></tr></tbody></table>\n<p>La heurística es el valor predeterminado correcto para tráfico de alto volumen que se parece, y puedes mejorarlo mucho con <code>custom_technical_keywords</code> para tu propio vocabulario de dominio. El clasificador LLM justifica su costo cuando los prompts son variados, cuando equivocarse de modelo es costoso y cuando medio segundo no importa — trabajo por lotes, agentes, cualquier cosa que ya esté tomando segundos.</p>\n<p>Lo que ninguno de ellos es, es gratis.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-de-problemas\">Solución de problemas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/#soluci%C3%B3n-de-problemas\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas\" title=\"Enlace directo al Solución de problemas\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-respuesta-dice-smart-router-en-lugar-de-un-nombre-de-modelo\">La respuesta dice <code>smart-router</code> en lugar de un nombre de modelo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/#la-respuesta-dice-smart-router-en-lugar-de-un-nombre-de-modelo\" class=\"hash-link\" aria-label=\"Enlace directo al la-respuesta-dice-smart-router-en-lugar-de-un-nombre-de-modelo\" title=\"Enlace directo al la-respuesta-dice-smart-router-en-lugar-de-un-nombre-de-modelo\" translate=\"no\">​</a></h3>\n<p><code>return_raw_model_name</code> no está configurado. Sin él, el enrutador repite el alias que pediste y no puedes decir qué sirvió la solicitud.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"cada-prompt-se-enruta-al-mismo-nivel\">Cada prompt se enruta al mismo nivel<a href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/#cada-prompt-se-enruta-al-mismo-nivel\" class=\"hash-link\" aria-label=\"Enlace directo al Cada prompt se enruta al mismo nivel\" title=\"Enlace directo al Cada prompt se enruta al mismo nivel\" translate=\"no\">​</a></h3>\n<p>Verifica el campo <code>signals</code> en la decisión de enrutamiento. Si es <code>null</code>, ninguna dimensión coincidió y el prompt puntuó 0.00, lo que cae en SIMPLE. Eso es una señal de que tu tráfico no utiliza el vocabulario que las listas de palabras clave predeterminadas esperan.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"las-decisiones-de-enrutamiento-no-están-en-el-registro-de-gastos\">Las decisiones de enrutamiento no están en el registro de gastos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/#las-decisiones-de-enrutamiento-no-est%C3%A1n-en-el-registro-de-gastos\" class=\"hash-link\" aria-label=\"Enlace directo al Las decisiones de enrutamiento no están en el registro de gastos\" title=\"Enlace directo al Las decisiones de enrutamiento no están en el registro de gastos\" translate=\"no\">​</a></h3>\n<p>Están en la solicitud enrutada, bajo <code>metadata.routing_decision</code>, no en la llamada del clasificador. Filtra con <code>map(select(.))</code> como arriba — la mitad de las filas son las llamadas del clasificador en sí y no llevan ninguna decisión.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-primera-solicitud-después-de-un-reinicio-es-mucho-más-lenta\">La primera solicitud después de un reinicio es mucho más lenta<a href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/#la-primera-solicitud-despu%C3%A9s-de-un-reinicio-es-mucho-m%C3%A1s-lenta\" class=\"hash-link\" aria-label=\"Enlace directo al La primera solicitud después de un reinicio es mucho más lenta\" title=\"Enlace directo al La primera solicitud después de un reinicio es mucho más lenta\" translate=\"no\">​</a></h3>\n<p>Inicio en frío. La primera clasificación aquí tomó 1449 ms contra aproximadamente 550 ms una vez caliente. Descarta la primera muestra al medir.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Route work to the right model</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-sigue\">¿Qué sigue?<a href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al ¿Qué sigue?\" title=\"Enlace directo al ¿Qué sigue?\" translate=\"no\">​</a></h2>\n<p>El gateway ahora decide qué modelo se ejecuta, y sabes lo que cuesta esa decisión en latencia y tokens. Aún reenvía cada prompt textualmente al modelo que gana — incluidos los que contienen nombres de clientes, correos electrónicos y claves API.</p>\n<p>La próxima publicación pone un filtro en ese camino.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lectura-adicional\">Lectura adicional<a href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/#lectura-adicional\" class=\"hash-link\" aria-label=\"Enlace directo al Lectura adicional\" title=\"Enlace directo al Lectura adicional\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.litellm.ai/docs/proxy/auto_routing\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Auto Routing — referencia de configuración</a></li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/\">Parte 1 — desplegar un gateway LLM en una instancia WEC</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/",
            "title": "Enrutamiento de complejidad de LiteLLM: el modelo adecuado para cada solicitud y su costo en latencia",
            "summary": "Cómo el enrutador de complejidad de LiteLLM decide qué modelo responde a una solicitud: las siete dimensiones de puntuación, la aritmética en un prompt real y una comparación medida del puntuador de palabras clave gratuito frente a un clasificador LLM.",
            "date_modified": "2026-08-19T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "llm",
                "gateway",
                "enrutamiento",
                "coste",
                "autoalojamiento",
                "litellm",
                "wec"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/",
            "content_html": "<div class=\"tutorialHero\"><svg viewBox=\"0 0 24 24\" fill=\"#2496ED\" aria-label=\"Docker\" class=\"tutorialHero__docker\"><path d=\"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z\"></path></svg><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/litellm-wordmark-86d6f0432720b27534e05bc579dfc29c.png\" alt=\"LiteLLM\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 5 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->5</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->5<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting an LLM gateway</div><ul class=\"skillTracker__steps\"><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">1</span><span class=\"skillTracker__skill\" data-state=\"current\">One endpoint, scoped keys</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/litellm-complexity-routing/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Route work to the right model</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-pii-llm-gateway-presidio/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Mask PII at the gateway</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mask-logs-not-responses-langfuse-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Clean traces, untouched answers</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/load-test-llm-gateway-blocking-callback/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove it holds under load</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>Así es como suele ir. Una aplicación necesita un modelo, así que pegas la clave API en\nsu <code>.env</code>. Luego una segunda aplicación necesita una. Luego un script. Seis meses después, la misma\nclave está en cinco lugares, nadie recuerda cuál de ellas sigue funcionando, y no\npuedes rotarla sin romper algo que solo descubrirás cuando se rompa.</p>\n<p>Un gateway es la solución aburrida. Un endpoint frente a cada modelo, un lugar que\nmantiene la credencial real, y una clave específica por aplicación que puedes revocar por su cuenta.\nEsta publicación despliega uno en una instancia WEC y lo apunta a la API de Inferencia WEC.</p>\n<!-- -->\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-estamos-poniendo-en-la-caja\">Lo que estamos poniendo en la caja<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/#lo-que-estamos-poniendo-en-la-caja\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que estamos poniendo en la caja\" title=\"Enlace directo al Lo que estamos poniendo en la caja\" translate=\"no\">​</a></h2>\n<p>El host ya ejecuta cinco otras pilas, lo cual es importante: este es el caso normal,\nno una VM limpia. Antes de agregar algo, verifica qué está escuchando y qué está libre:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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>El puerto 4000 está libre. Postgres 5432 pertenece al host y 5433 a Langfuse, así que la\nbase de datos del gateway no obtiene ninguno: no publicará un puerto en absoluto.</p>\n<p>Luego confirma que el backend responde antes de poner algo frente a él:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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>Trece modelos en una clave. Eso es lo que estamos a punto de dejar de repartir.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-pila\">La pila<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/#la-pila\" class=\"hash-link\" aria-label=\"Enlace directo al La pila\" title=\"Enlace directo al La pila\" translate=\"no\">​</a></h2>\n<p>Dos contenedores: el gateway y un Postgres para su propio estado. Crea un directorio y\nescribe el archivo de composición:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Cuatro de esas líneas están haciendo trabajo real:</p>\n<p><code>127.0.0.1:4000:4000</code> mantiene el gateway fuera de Internet público. Mantiene cada\ncredencial de modelo que posees, así que publicarlo en <code>0.0.0.0</code> pondría todas\ndetrás de una sola contraseña compartida. <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/\">Parte 1 de la serie de endurecimiento</a> es la versión larga de por qué\neso es un mal intercambio.</p>\n<p>La base de datos no tiene <code>ports:</code> en absoluto. Nada fuera de la red de composición necesita\nalcanzarla, y 5432 ya estaba ocupado.</p>\n<p><code>user: \"999:999\"</code> ejecuta Postgres como su usuario no root incorporado desde el primer arranque.\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/\">Parte 2</a> cubre lo que eso te ofrece\ny cómo encontrar el UID correcto para una imagen en lugar de adivinar.</p>\n<p><code>condition: service_healthy</code> importa más de lo que parece. El gateway ejecuta migraciones de base de datos al inicio: sin ello, compite con Postgres y entra en un bucle de fallos.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"informándole-sobre-los-modelos\">Informándole sobre los modelos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/#inform%C3%A1ndole-sobre-los-modelos\" class=\"hash-link\" aria-label=\"Enlace directo al Informándole sobre los modelos\" title=\"Enlace directo al Informándole sobre los modelos\" 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> es el alias que tus aplicaciones piden. <code>model:</code> es lo que el gateway realmente\nllama. Esa indirecta es la mayor parte del valor: cambia <code>qwen-mid</code> para apuntar a otro lugar\nel próximo mes y ningún cliente cambia.</p>\n<p>El prefijo <code>openai/</code> dice \"habla el protocolo OpenAI a un <code>api_base</code> personalizado.\" La\nAPI de Inferencia WEC es compatible con OpenAI, así que eso es todo lo que se necesita.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"secretos\">Secretos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/#secretos\" class=\"hash-link\" aria-label=\"Enlace directo al Secretos\" title=\"Enlace directo al Secretos\" translate=\"no\">​</a></h2>\n<p>Genera las contraseñas en lugar de inventarlas:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash 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>Luego pon tu clave WEC real en lugar de <code>your-wec-key-here</code>. La clave maestra es la\ncredencial raíz del gateway: puede crear claves, eliminar claves y acceder a cada modelo,\nasí que va en tu gestor de contraseñas y en ningún otro lugar.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"levantándolo\">Levantándolo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/#levant%C3%A1ndolo\" class=\"hash-link\" aria-label=\"Enlace directo al Levantándolo\" title=\"Enlace directo al Levantándolo\" 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\">Salida</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\"> ✔ Imagen ghcr.io/berriai/litellm:main-stable Descargada          58.2s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> ✔ Red llm-gateway_default               Creada          0.1s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> ✔ Volumen llm-gateway_gateway-db             Creado          0.1s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> ✔ Contenedor llm-gateway-db                  Saludable          8.8s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> ✔ Contenedor llm-gateway                     Creado          0.2s</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker compose up descargando la imagen y creando ambos contenedores\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 1.</strong> Una descarga, dos contenedores, una red y un volumen.</p>\n<p><code>Creado</code> en lugar de <code>Iniciado</code> en esa última línea es normal para la salida de composición,\npero verifica de todos modos — y verifica cuánto te costó la imagen en disco mientras estás allí:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> 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\">Salida</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\">NOMBRE             IMAGEN                                 SERVICIO   ESTADO                   PUERTOS</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">llm-gateway      ghcr.io/berriai/litellm:main-stable   litellm   Arriba 3 minutos             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        Arriba 3 minutos (saludable)   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\">Sistema de archivos      Tamaño  Usado Disponible Uso% Montado en</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>Nota la columna <code>PUERTOS</code> de la base de datos: <code>5432/tcp</code> sin enlace de host delante de\nella. La imagen del gateway es de 1.16GB, lo cual en una caja que ya ejecuta cinco pilas no es\nnada despreciable. Verifica que tienes espacio antes de comenzar, no después.</p>\n<p>Dos líneas en el registro de inicio valen la pena leer, porque ambas se ven peores de lo que son:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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\">✅ Diferencia de migración aplicada con éxito</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">INFO:     Inicio de la aplicación completo.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">INFO:     Uvicorn ejecutándose en http://0.0.0.0:4000 (Presiona CTRL+C para salir)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">register_model: model=openai/Qwen2.5-3B-Instruct no en el mapa de costos incorporado y ningún</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">prefijo/variante de región coincidió; los campos de costo de caché se establecerán en 0.</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Estado del contenedor, uso de disco y el registro de inicio con migraciones y advertencias del mapa de costos\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 2.</strong> Migraciones aplicadas, inicio de la aplicación completo y una advertencia del mapa de costos por modelo.</p>\n<p><code>0.0.0.0:4000</code> es el enlace <em>dentro</em> del contenedor. La publicación en el host sigue siendo\n<code>127.0.0.1</code>, que es lo que realmente controla el acceso. Y la advertencia del mapa de costos es\nmás específica de lo que parece: se trata de la fijación de precios de caché específicamente. La\nfijación de precios de tokens regulares sigue ocurriendo, lo cual resulta ser importante más adelante.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-primera-llamada\">La primera llamada<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/#la-primera-llamada\" class=\"hash-link\" aria-label=\"Enlace directo al La primera llamada\" title=\"Enlace directo al La primera llamada\" 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\":\"Responde exactamente: 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\">Salida</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\": \"Proceso de Pensamiento:\\n\\n1.  **Analiza la Solicitud:**\\n    *   Entrada: \\\"\",</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=\"Respuesta cruda con finish_reason length, reasoning_content poblado y content null\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 3.</strong> Una llamada exitosa sin nada en ella.</p>\n<p><code>content: null</code>. La llamada funcionó: se enruta, llegó al backend, contó tokens\n— pero no hay respuesta en ella.</p>\n<p><code>Qwen3.5-9B</code> es un modelo de razonamiento. Gastó todos los veinte tokens pensando y no le quedaron\npara hablar. El movimiento obvio es darle más espacio:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\":\"Responde exactamente: 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\">Salida</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>Doscientos tokens, aún nada. Y esta es la parte que vale la pena pausar: una ejecución anterior de ese mismo comando <em>sí</em> respondió, con 192 tokens. Mismo modelo, mismo aviso,\ndiferente cantidad de pensamiento. No hay un <code>max_tokens</code> que puedas establecer que garantice\nuna respuesta, porque la longitud del razonamiento no es fija.</p>\n<p>Ahora el mismo aviso a través del modelo pequeño, con los veinte tokens originales:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\":\"Responde exactamente: 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\">Salida</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=\"Modelo de razonamiento quemando 200 tokens de finalización y devolviendo null, modelo pequeño respondiendo en 3\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 4.</strong> Doscientos tokens y ninguna respuesta, contra tres tokens y la respuesta.</p>\n<p>Nada está roto aquí. El modelo de razonamiento está haciendo exactamente lo que se supone que debe hacer,\nen una pregunta que no lo necesitaba. La solución no es un presupuesto de tokens más grande: no\nenviar trabajo trivial a ese modelo en primer lugar. Lo cual es todo el argumento\npara el enrutamiento, y el tema de la próxima publicación.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes poner un gateway frente a varios modelos, dar a cada uno un alias que tus aplicaciones\nllamen en lugar de un ID de modelo de proveedor, y leer una respuesta lo suficientemente bien como\npara distinguir un modelo de razonamiento de uno simple.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"probando-que-la-puerta-está-cerrada\">Probando que la puerta está cerrada<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/#probando-que-la-puerta-est%C3%A1-cerrada\" class=\"hash-link\" aria-label=\"Enlace directo al Probando que la puerta está cerrada\" title=\"Enlace directo al Probando que la puerta está cerrada\" translate=\"no\">​</a></h2>\n<p>Un gateway que mantiene cada credencial que posees debería rechazar a cualquiera sin una clave.\nVale la pena verificar en lugar de asumir:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">\"sin clave:     %{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\":\"hola\"}]}'</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\">\"clave incorrecta:  %{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\":\"hola\"}]}'</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\">\"clave maestra: %{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\":\"hola\"}],\"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\">Salida</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\">sin clave:     401</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">clave incorrecta:  401</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">clave maestra: 200</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Tres llamadas curl devolviendo 401, 401 y 200\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 5.</strong> Tanto anónimos como falsificados rechazados, la clave real a través.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-interfaz-de-administración\">La interfaz de administración<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/#la-interfaz-de-administraci%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al La interfaz de administración\" title=\"Enlace directo al La interfaz de administración\" translate=\"no\">​</a></h2>\n<p>El gateway incluye una interfaz web, y está vinculada a localhost — que es el\npunto, así que accede a ella a través de SSH en lugar de abrir un puerto:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Deja eso funcionando y abre <code>http://localhost:4000/ui</code>. Nombre de usuario <code>admin</code>, la contraseña\nes la clave maestra de tu <code>.env</code>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La pantalla de inicio de sesión del gateway, pidiendo admin más la clave maestra\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 6.</strong> La página de inicio de sesión indica sus propias credenciales predeterminadas, lo cual es un buen\nrecordatorio de que la clave maestra es lo único que está ahí.</p>\n<p>Haz clic en <strong>Modelos + Endpoints</strong>:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Gestión de modelos listando qwen-small, qwen-mid, qwen-large y embeddings, todos mostrando costo cero\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 7.</strong> Todos los cuatro alias registrados, cada uno mapeado a su objetivo <code>openai/...</code>.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"claves-que-no-pueden-hacer-todo\">Claves que no pueden hacer todo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/#claves-que-no-pueden-hacer-todo\" class=\"hash-link\" aria-label=\"Enlace directo al Claves que no pueden hacer todo\" title=\"Enlace directo al Claves que no pueden hacer todo\" translate=\"no\">​</a></h2>\n<p>La clave maestra puede acceder a cada modelo y crear más claves. Ninguna aplicación debería\ntenerla nunca. En su lugar, emite una clave virtual específica para lo que esa aplicación realmente necesita.</p>\n<p><strong>Claves Virtuales</strong> → <strong>+ Crear Nueva Clave</strong>:</p>\n<table><thead><tr><th>Campo</th><th>Valor</th></tr></thead><tbody><tr><td>Propietario</td><td>Tú</td></tr><tr><td>Equipo</td><td>dejar vacío</td></tr><tr><td>Nombre de Clave</td><td><code>demo-app</code></td></tr><tr><td>Modelos</td><td><code>qwen-small</code></td></tr><tr><td>Presupuesto Máximo (USD)</td><td><code>0.10</code></td></tr><tr><td>Restablecer Presupuesto</td><td>diario</td></tr></tbody></table>\n<p>Dos cosas sobre este formulario. <strong>Presupuesto Máximo</strong> vive bajo <strong>Configuraciones Opcionales</strong>,\nque comienza colapsada — fácil de perder y luego preguntarse dónde fue el campo de presupuesto. Y\ndejar <strong>Modelos</strong> vacío significa <em>todos los modelos</em>, lo opuesto a lo que quieres aquí.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El formulario de creación de clave con qwen-small seleccionado y un presupuesto diario de diez centavos\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 8.</strong> Específico para un modelo, limitado a diez centavos al día.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El diálogo Guardar tu Clave, advirtiendo que la clave no puede ser vista nuevamente\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 9.</strong> El diálogo lo dice claramente. Créelo.</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>Copie la clave ahora</div><div class=\"admonitionContent_BuS1\"><p>La clave generada se muestra una vez y nunca más. Cierra esto sin copiar y\ntu única opción es eliminar la clave y crear otra.</p></div></div>\n<p>Ahora la parte que justifica todo el ejercicio. La misma clave, dos modelos:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\":\"di hola\"}],\"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\">Salida</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\">¡Hola! ¿Cómo puedo ayudarte hoy?</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\":\"di hola\"}],\"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\">Salida</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\": \"clave no permitida para acceder al modelo. Esta clave solo puede acceder a modelos=['qwen-small']. Intentó acceder a 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=\"La misma clave virtual respondiendo en qwen-small y siendo rechazada con un 403 en qwen-large\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 10.</strong> Una credencial, un modelo. Filtrarla y el radio de explosión es esa única línea.</p>\n<p>Compara eso con la situación en la que comenzamos: una clave en cinco archivos <code>.env</code>, con acceso\na todos los trece modelos y sin forma de saber qué aplicación la está utilizando.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes emitir una credencial específica por aplicación, restringirla a modelos específicos\ncon un límite de gasto, y revocarla por su cuenta sin tocar nada más.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-el-gateway-registró\">Lo que el gateway registró<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/#lo-que-el-gateway-registr%C3%B3\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que el gateway registró\" title=\"Enlace directo al Lo que el gateway registró\" translate=\"no\">​</a></h2>\n<p>Cada llamada aterriza en un registro de gastos:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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=\"Filas del registro de gastos mostrando conteos de tokens y una cifra en dólares por llamada\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 11.</strong> Tokens contados por llamada, con un precio adjunto a cada uno.</p>\n<p>Ese <code>select(.spend &gt; 0)</code> no es cosmético. Las llamadas fallidas también se registran, con cero\ntokens y cero gasto: los 401 de antes están todos ahí con <code>status: \"failure\"</code>. Útil por sí mismo: el registro de gastos también sirve como un registro de\nalguien intentando claves que no funcionan.</p>\n<p>Esas cifras en dólares son reales. Coinciden con la fijación de precios por modelo publicada para la\nAPI que este gateway está llamando: $0.70 por millón de tokens de entrada y $3.70 por millón\nde salida para <code>Qwen3.5-9B</code>, las mismas tarifas que ves listadas contra cada modelo antes\nde elegir uno. Así que el registro de gastos es un registro genuino de lo que costó cada llamada,\ndesglosado por clave y por modelo, desde la primera solicitud.</p>\n<p>Dos cosas más en la entrada completa del registro, ambas útiles:</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>El gateway mide su propia sobrecarga y la informa por solicitud: 36ms aquí, que es un número real al que puedes exigirle. Y los avisos y respuestas no se almacenan por defecto: el registro sabe que ocurrió una llamada y cuánto costó, no lo que se dijo.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-de-problemas\">Solución de problemas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/#soluci%C3%B3n-de-problemas\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas\" title=\"Enlace directo al Solución de problemas\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-contenedor-del-gateway-se-inicia-y-sale-inmediatamente\">El contenedor del gateway se inicia y sale inmediatamente<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/#el-contenedor-del-gateway-se-inicia-y-sale-inmediatamente\" class=\"hash-link\" aria-label=\"Enlace directo al El contenedor del gateway se inicia y sale inmediatamente\" title=\"Enlace directo al El contenedor del gateway se inicia y sale inmediatamente\" translate=\"no\">​</a></h3>\n<p>Casi siempre es la base de datos. Verifica <code>docker compose logs litellm</code> para errores de migración: si el gateway se levantó antes de que Postgres estuviera listo, falta la condición <code>depends_on</code> o el healthcheck no está pasando.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"content-regresa-null-con-finish_reason-length\"><code>content</code> regresa <code>null</code> con <code>finish_reason: \"length\"</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/#content-regresa-null-con-finish_reason-length\" class=\"hash-link\" aria-label=\"Enlace directo al content-regresa-null-con-finish_reason-length\" title=\"Enlace directo al content-regresa-null-con-finish_reason-length\" translate=\"no\">​</a></h3>\n<p>El modelo es un modelo de razonamiento y utilizó todo tu presupuesto de tokens pensando. Aumentar\n<code>max_tokens</code> ayuda pero no garantiza nada: el mismo aviso con 200 tokens respondió en una ejecución y devolvió <code>null</code> en la siguiente. Para trabajos que no necesitan\nrazonamiento, envíalos a un modelo que no haga ninguno.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-interfaz-de-administración-no-se-carga\">La interfaz de administración no se carga<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/#la-interfaz-de-administraci%C3%B3n-no-se-carga\" class=\"hash-link\" aria-label=\"Enlace directo al La interfaz de administración no se carga\" title=\"Enlace directo al La interfaz de administración no se carga\" translate=\"no\">​</a></h3>\n<p>Está vinculada a <code>127.0.0.1</code>, así que es inaccesible desde cualquier lugar que no sea la propia caja.\nUsa un túnel SSH. Si el túnel está activo y la página aún no se carga, confirma que el\npuerto en <code>docker compose ps</code> coincide con el que has reenviado.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"una-cadena-de-shell-silenciosamente-no-hizo-nada\">Una cadena de shell silenciosamente no hizo nada<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/#una-cadena-de-shell-silenciosamente-no-hizo-nada\" class=\"hash-link\" aria-label=\"Enlace directo al Una cadena de shell silenciosamente no hizo nada\" title=\"Enlace directo al Una cadena de shell silenciosamente no hizo nada\" translate=\"no\">​</a></h3>\n<p>Las cadenas unidas con <code>&amp;&amp;</code> se detienen en el primer comando que falla, y un <code>grep</code> fallido\ncuenta como un fallo. Sourcing un archivo env cuyo nombre de variable adivinaste mal\ndeja un marcador de posición en su lugar y todo parece bien hasta un error de autenticación tres\npasos después. Verifica los marcadores de posición explícitamente:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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=\"eliminando-una-clave-virtual\">Eliminando una clave virtual<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/#eliminando-una-clave-virtual\" class=\"hash-link\" aria-label=\"Enlace directo al Eliminando una clave virtual\" title=\"Enlace directo al Eliminando una clave virtual\" translate=\"no\">​</a></h3>\n<p>Desde la terminal, por 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=\"qué-sigue\">¿Qué sigue?<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al ¿Qué sigue?\" title=\"Enlace directo al ¿Qué sigue?\" translate=\"no\">​</a></h2>\n<p>Ahora tienes un endpoint, una credencial por aplicación y un registro de cada llamada. La\npregunta obvia es la que planteó la Figura 4: si el modelo pequeño responde en 3 tokens\nlo que el modelo de razonamiento gasta 200 sin responder en absoluto, ¿por qué se enruta algo\nal costoso por defecto?</p>\n<p>La próxima publicación pone al gateway a cargo de esa decisión y mide lo que cuesta pedirle.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lectura-adicional\">Lectura adicional<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/#lectura-adicional\" class=\"hash-link\" aria-label=\"Enlace directo al Lectura adicional\" title=\"Enlace directo al Lectura adicional\" 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 — dependencias de servicio y healthchecks</a></li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/\">Endurecer redes Docker para hosts multi-agente</a></li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/\">Root por defecto: endurecimiento del privilegio del contenedor</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/deploy-llm-gateway-wec-instance/",
            "title": "Un endpoint, muchos modelos: despliega un gateway LLM en una instancia WEC",
            "summary": "Cada aplicación en una caja que tiene la misma clave API es un problema esperando a suceder. Un gateway soluciona eso: un endpoint, claves específicas por aplicación, presupuestos por clave y un registro de quién gastó qué. Desplegado de verdad en un host que ya ejecuta cinco otras pilas, incluyendo las partes que nos sorprendieron.",
            "date_modified": "2026-08-13T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "llm",
                "gateway",
                "docker",
                "autoalojamiento",
                "api",
                "litellm",
                "wec"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/",
            "content_html": "<div class=\"tutorialHero\"><svg viewBox=\"0 0 24 24\" fill=\"#2496ED\" aria-label=\"Docker\" class=\"tutorialHero__docker\"><path d=\"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z\"></path></svg><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2255\" height=\"527\" fill=\"none\" viewBox=\"0 0 2255 527\" class=\"tutorialHero__langfuse\"><path fill=\"#1B1917\" d=\"M652.669 116.433c0-10.261-7.683-17.956-17.926-17.956H607V60h87.923v316.366h-42.254zM804.056 379.786c-39.693 0-72.131-27.362-72.131-68.831 0-41.042 28.596-69.686 84.935-69.686h39.693c7.256 0 12.805-5.558 12.805-12.826v-8.55c0-25.651-20.487-38.905-40.547-38.905-18.353 0-33.291 8.123-41.828 26.934h-45.668c11.95-44.462 46.095-65.41 88.349-65.41 40.547 0 82.374 22.231 82.374 77.808v156.046h-42.68v-14.109c0-5.13-5.549-7.695-9.817-4.702-16.646 11.97-32.438 22.231-55.485 22.231m5.975-38.477c18.78 0 34.572-9.406 52.498-27.362 4.695-4.702 6.829-10.688 6.829-17.1v-6.413c0-7.268-5.549-12.826-12.805-12.826H815.58c-27.743 0-40.974 12.398-40.974 31.637 0 17.956 12.377 32.064 35.425 32.064M961.855 376.366V147.642h42.255v19.666c0 5.13 6.4 6.84 10.24 2.992 13.23-12.825 31.59-27.788 60.61-27.788 36.71 0 70.85 23.086 70.85 75.671v158.183h-42.25V227.161c0-29.499-17.93-45.745-39.7-45.745-20.91 0-35 11.971-49.93 29.499-7.26 8.978-9.82 19.238-9.82 30.354v135.097zM1287.48 467c-52.5 0-85.79-24.796-95.61-63.273h46.1c7.25 15.818 20.06 25.651 45.67 25.651 34.14 0 55.48-20.093 55.48-65.838v-7.268c0-5.13-4.27-7.695-9.39-3.42-14.08 12.398-32.01 20.093-49.08 20.093-58.05 0-96.46-44.889-96.46-115.003 0-70.113 44.39-115.43 98.17-115.43 15.79 0 30.3 4.275 44.38 14.963 5.98 4.275 12.38.855 12.38-5.986v-3.847h42.26V363.54c0 72.678-43.97 103.46-93.9 103.46m-2.56-132.959c19.2 0 33.29-8.55 44.81-20.949 7.26-8.122 9.39-13.68 9.39-26.506v-61.563c0-12.826-2.13-20.948-10.67-29.071-9.39-8.978-22.62-14.964-39.69-14.964-35 0-61.89 29.072-61.89 76.954 0 47.883 24.76 76.099 58.05 76.099M1455.92 199.372c0-7.268-5.97-13.253-13.23-13.253h-32.44v-38.477h32.44c7.26 0 13.23-5.985 13.23-13.253v-5.986c0-45.744 23.48-68.403 69.15-68.403h29.02v38.477h-29.45c-17.5 0-26.46 9.833-26.46 29.926v5.986c0 7.268 5.97 13.253 13.23 13.253h42.68v38.477h-42.68c-7.26 0-13.23 5.985-13.23 13.253v176.994h-42.26zM1652.02 381.496c-35.85 0-69.14-23.086-69.14-75.671V147.642h42.25v150.06c0 29.499 17.07 44.889 37.13 44.889 21.77 0 35.85-11.97 50.79-29.499 7.26-8.977 9.82-19.238 9.82-30.354V147.642h42.25v228.724h-42.25V356.7c0-5.131-6.4-6.841-10.24-2.993-13.24 12.826-31.59 27.789-60.61 27.789M1893.57 381.496c-38.84 0-79.39-19.239-90.06-65.838h43.54c6.4 17.528 23.9 29.498 44.81 29.498 23.05 0 37.13-13.68 37.13-30.353 0-16.246-11.09-25.224-28.59-30.354l-36.28-10.261c-31.58-8.978-55.06-29.499-55.06-64.556 0-38.049 35.43-67.12 75.55-67.12 32.01 0 70.85 14.535 81.09 65.41h-40.55c-5.55-17.528-20.06-29.071-40.54-29.071-20.06 0-34.58 12.398-34.58 28.216 0 13.253 8.11 23.086 27.32 28.644l34.15 9.833c32.43 9.406 58.47 29.072 58.47 66.693 0 39.332-34.15 69.259-76.4 69.259M2098.54 381.496c-61.46 0-102.01-51.73-102.01-119.706s43.11-119.278 101.58-119.278c63.6 0 96.89 51.302 96.89 109.872v23.087h-144.26c-5.98 0-8.54 3.847-7.26 13.68 4.7 32.064 30.31 54.295 55.49 54.295 18.78 0 35-9.405 45.67-27.788h44.81c-16.22 40.187-49.94 65.838-90.91 65.838m43.11-141.51c6.83 0 9.39-3.42 7.68-14.108-4.69-26.506-24.33-45.317-51.22-45.317-25.6 0-47.37 18.811-54.2 45.745-2.56 9.833.85 13.68 6.83 13.68z\"></path><path fill=\"#FF5D5F\" d=\"m286.292 286.105 34.597 27.791s26.473-19.661 45.941-22.545c20.418-3.025 42.202 8.359 62.388 21.93 30.489 20.498 56.149 46.508 56.149 46.508l30.06-29.493s-82.879-89.795-148.597-81.672c-43.105 5.328-80.538 37.481-80.538 37.481\"></path><path fill=\"#4E9CFF\" d=\"M88.358 114.862 60 146.056s79.009 73.732 141.224 73.732c28.358 0 67.684-22.216 101.523-51.079 19.283-16.448 40.835-35.13 62.388-35.13 14.487 0 33.594 7.673 51.612 27.824 0 0 11.63-6.974 18.716-11.985 6.228-4.404 15.479-11.91 15.479-11.91-25.918-27.663-63.407-47.883-85.807-45.9-36.299.005-62.388 22.601-94.717 48.735s-45.94 36.907-69.194 36.907c-39.134 0-112.866-62.388-112.866-62.388M88.358 352.463 60 321.269s79.009-73.732 141.224-73.732c28.358 0 67.684 22.216 101.523 51.079 19.283 16.448 40.835 35.13 62.388 35.13 14.556 0 33.518-7.989 51.612-28.358 0 0 10.877 6.705 17.582 11.344 6.894 4.769 17.015 12.655 17.015 12.655-25.931 27.883-63.693 48.323-86.209 46.33-36.299-.005-57.851-19.24-90.179-45.374-32.329-26.133-50.478-40.268-73.732-40.268-39.134 0-112.866 62.388-112.866 62.388M458.142 185.149c-7.378 5.1-19.283 12.478-19.283 12.478s6.806 14.746 6.806 34.597-6.239 36.866-6.239 36.866 10.688 6.675 17.582 11.343c7.162 4.849 18.149 13.045 18.149 13.045s13.045-27.224 13.045-61.254-13.045-59.552-13.045-59.552-10.236 7.792-17.015 12.477\"></path><path fill=\"#FF5D5F\" d=\"m287.995 180.612 32.895-27.224s26.473 19.046 45.941 21.93c20.417 3.026 42.202-8.359 62.388-21.93 30.489-20.498 56.149-46.507 56.149-46.507l30.06 29.492s-82.879 89.795-148.597 81.672c-43.105-5.328-78.836-37.433-78.836-37.433M208.601 91c42.538 0 78.264 36.299 78.264 36.299s-9.941 7.832-16.448 13.045c-6.777 5.429-17.582 14.179-17.582 14.179s-18.711-19.851-44.234-19.851c-10.465 0-24.066 6.286-38.567 18.716-11.188 9.591-22.829 21.514-30.627 36.299-6.743 12.784-10.42 27.85-10.776 43.672-.446 19.873 6.597 40.704 18.149 57.283 7.743 11.112 16.983 19.474 26.657 26.657 12.555 9.322 25.648 15.881 35.164 15.881 10.166 0 19.306-3.533 26.09-6.806 10.776-6.239 19.278-13.612 19.278-13.612l33.463 27.791s-13.612 13.612-32.323 23.821c-12.091 5.963-27.632 11.91-46.508 11.91-18.862 0-40.767-10.022-61.254-25.522-13.244-10.021-26.225-21.895-36.298-36.299-16.51-23.607-25.017-52.328-24.96-81.104.057-29.136 9.451-57.993 26.094-81.672C138.273 117.657 176.86 91 208.601 91\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Lock down Docker networks</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">2</span><span class=\"skillTracker__skill\" data-state=\"current\">Run containers as non-root</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Membership, not just an account</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">An identity for the agent, not a key</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">A tool server that checks who is asking</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Where the agent can go, not just what it can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/\"><span class=\"skillTracker__dot\" data-state=\"locked\">8</span><span class=\"skillTracker__skill\" data-state=\"locked\">Move the daemon off root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/\"><span class=\"skillTracker__dot\" data-state=\"locked\">9</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run the model's own code without trusting it</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">A scope per tool, and a refusal clients can act on</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/\">Parte 1</a> de esta serie auditó una caja multi-agente en vivo y encontró cada pregunta sobre exposición de red que valía la pena hacer. Una línea de esa auditoría no se siguió: <em>\"el Postgres, ClickHouse y Redis detrás de Langfuse estaban publicados en <code>127.0.0.1</code> en lugar del mundo — alguien tomó una buena decisión allí. Mantén ese pensamiento.\"</em></p>\n<p>Aquí está la otra mitad de ese pensamiento: conseguir la red correcta no dice nada sobre lo que sucede <em>después</em> de que alguien ya esté dentro de un contenedor. Si el proceso que se ejecuta allí es root, un compromiso comienza con las llaves de todo el sistema de archivos. Así que verificamos — en la misma caja, la misma pila de Langfuse que la Parte 1 ya alabó — si \"red correcta\" también significaba \"privilegio correcto.\" No lo era, para dos de los seis contenedores. Aquí está cómo se veía realmente la solución, incluyendo la parte que se rompió.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"verificando-quién-es-realmente-root\">Verificando quién es realmente root<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/#verificando-qui%C3%A9n-es-realmente-root\" class=\"hash-link\" aria-label=\"Enlace directo al Verificando quién es realmente root\" title=\"Enlace directo al Verificando quién es realmente root\" translate=\"no\">​</a></h2>\n<p>Docker no te dice esto por defecto — un contenedor puede estar publicado correctamente, saludable y ejecutándose como <code>root</code> todo el tiempo, sin que nada en <code>docker ps</code> lo sugiera. La única forma de saberlo es preguntar directamente al contenedor:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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=\"Ambas verificaciones de id -u devolviendo 0 — ambos contenedores ejecutándose como root\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 1.</strong> La verificación que la mayoría de las configuraciones nunca ejecutan: ambas llamadas a <code>id -u</code> regresan <code>0</code>. Saludables, publicados correctamente, y root todo el tiempo.</p>\n<p><code>0</code> es root. Ambos estaban ejecutándose como root, y al mirar el archivo de composición se explica por qué — ninguno de los servicios tiene una directiva <code>user:</code> en absoluto:</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 (antes)</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>Nadie decidió que estos debían ejecutarse como root. Es simplemente lo que sucede cuando no configuras nada — la misma forma que el hallazgo de la Parte 1 de que una contraseña de Redis no configurada predetermina la falta de autenticación. El servicio <code>clickhouse</code> en el mismo archivo muestra la solución ya medio hecha, sentada justo allí para comparación:</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 (ya correcto)</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>Una línea. Alguien lo configuró para ClickHouse y nunca llegó a los otros dos.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"encontrando-el-usuario-correcto-no-una-suposición\">Encontrando el usuario correcto, no una suposición<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/#encontrando-el-usuario-correcto-no-una-suposici%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al Encontrando el usuario correcto, no una suposición\" title=\"Enlace directo al Encontrando el usuario correcto, no una suposición\" translate=\"no\">​</a></h2>\n<p>No inventes un UID — ambas imágenes oficiales ya incluyen un usuario no root construido para exactamente esto, y adivinar mal significa un error de permisos o, peor aún, un UID que silenciosamente no coincide con el que los propios archivos de la imagen son propiedad:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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>Ambas imágenes utilizan <code>999:999</code> para su usuario no root incorporado. Agrégalo al archivo de composición:</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 (después)</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=\"la-conversión-en-sí-fue-un-no-evento\">La conversión en sí fue un no evento<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/#la-conversi%C3%B3n-en-s%C3%AD-fue-un-no-evento\" class=\"hash-link\" aria-label=\"Enlace directo al La conversión en sí fue un no evento\" title=\"Enlace directo al La conversión en sí fue un no evento\" 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\">Salida</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\"> ✔ Contenedor langfuse-postgres-1 recreado</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> ✔ Contenedor langfuse-redis-1 recreado</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\">Salida</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 mostrando ambos contenedores saludables, y id -u ahora devolviendo 999\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 2.</strong> Los mismos dos contenedores, segundos después de <code>--force-recreate</code>: ambos <code>healthy</code>, ambos <code>id -u</code> ahora <code>999</code> en lugar de <code>0</code>.</p>\n<p>Sin errores de permisos, sin dramas de propiedad, ambos saludables en segundos. Eso vale la pena decirlo claramente porque no siempre es cierto — si estos volúmenes se hubieran inicializado como root en algún lugar upstream y nunca se les hubiera hecho <code>chown</code>, este comando exacto puede fallar con <code>permission denied</code> en el directorio de datos. No falló aquí, pero revisa tus propios registros después de recrear, no asumas que \"sin salida\" significa \"sin problema.\"</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes verificar si un contenedor en ejecución es realmente root (<code>docker exec &lt;container&gt; id -u</code>), encontrar el usuario no root previsto de su imagen en lugar de adivinar un UID (<code>docker exec &lt;container&gt; id &lt;username&gt;</code>), y convertirlo con una línea <code>user:</code> en la composición.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-parte-que-realmente-se-rompió\">La parte que realmente se rompió<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/#la-parte-que-realmente-se-rompi%C3%B3\" class=\"hash-link\" aria-label=\"Enlace directo al La parte que realmente se rompió\" title=\"Enlace directo al La parte que realmente se rompió\" translate=\"no\">​</a></h2>\n<p>Todo lo anterior fue limpio. Luego, los registros de un contenedor completamente diferente contaron una historia diferente:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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\">Error de Redis getaddrinfo ENOTFOUND redis</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Error de Redis connect ECONNREFUSED 172.19.0.5:6379</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">El trabajo de cola mixpanel-integration-processing-queue falló: Error: Tiempo de espera de socket.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Esperando datos, pero no se recibió ninguno en 30000ms.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">El trabajo de cola trace-delete falló: Error: Tiempo de espera de socket. Esperando datos, pero no se recibió ninguno en 30000ms.</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"registros de langfuse-worker mostrando repetidos tiempos de espera de socket de 30 segundos contra Redis, uno por cola\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 3.</strong> Un contenedor que nadie tocó, fallando de todos modos — cada trabajo de cola con el mismo tiempo de espera de socket de 30 segundos contra Redis, mucho después de la recreación que lo causó.</p>\n<p><code>langfuse-worker</code> nunca tocó la propiedad de Postgres o Redis — solo mantiene una conexión activa a Redis, y <code>--force-recreate</code> derribó el contenedor al que apuntaba esa conexión. La propia verificación de salud de Docker decía que Redis estaba <code>healthy</code> de nuevo mucho antes de que el trabajador se rindiera al reintentar; ioredis simplemente no se reconecta limpiamente por sí mismo aquí, y siguió fallando con tiempos de espera de socket de 30 segundos mucho después de que la dependencia de la que dependía volvió.</p>\n<p>La solución es el reinicio del propio servicio dependiente, no otra mirada a 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\">Salida</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    La conexión a Redis se ha cerrado.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">2026-08-10T19:40:22.769Z info    Apagado completo, saliendo del proceso...</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\">Salida</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=\"registros de langfuse-worker después del reinicio, mostrando cada ejecutor de cola comenzando limpiamente\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 4.</strong> El mismo contenedor, un reinicio después — cada ejecutor de cola comienza limpio, sin errores de Redis en ninguna parte del registro.</p>\n<p>Inicio limpio de la cola, sin errores de Redis. Esa es toda la lección: <strong>endurecer un contenedor no se limita a ese contenedor.</strong> Cualquier cosa que mantenga una conexión activa con la cosa que acabas de recrear necesita su propio reinicio, a propósito, no como un pensamiento posterior que descubres a partir de un registro de errores veinte minutos después.</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>Esto es lo que debes recordar</div><div class=\"admonitionContent_BuS1\"><p><code>--force-recreate</code> en una dependencia compartida (una base de datos, una cola, un caché) puede romper silenciosamente cada servicio que ya tenía una conexión abierta a ella — incluso después de que la dependencia misma informe que está saludable de nuevo. Reinicia explícitamente a los dependientes; no asumas que lo notarán por sí mismos.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-encontramos-y-lo-que-costó-arreglar\">Lo que encontramos y lo que costó arreglar<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/#lo-que-encontramos-y-lo-que-cost%C3%B3-arreglar\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que encontramos y lo que costó arreglar\" title=\"Enlace directo al Lo que encontramos y lo que costó arreglar\" translate=\"no\">​</a></h2>\n<table><thead><tr><th>Contenedor</th><th>Antes</th><th>Solución</th><th>¿Rompe algo?</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 — recreado limpio</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 — recreado limpio</td></tr><tr><td><code>langfuse-clickhouse-1</code></td><td>ya <code>101:101</code></td><td>ninguna necesaria</td><td>—</td></tr><tr><td><code>langfuse-langfuse-worker-1</code></td><td>intocable</td><td>ninguna — solo necesitaba un reinicio</td><td>Sí — perdió su conexión a Redis después de que los <em>otros</em> dos contenedores fueron recreados</td></tr></tbody></table>\n<p>Dos soluciones de una línea. El costo real no fue la solución — fue notar que el tercer contenedor que nadie tocó fue el que se rompió.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-de-problemas\">Solución de problemas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/#soluci%C3%B3n-de-problemas\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas\" title=\"Enlace directo al Solución de problemas\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"docker-compose-restart-dijo-no-se-proporcionó-archivo-de-configuración-no-encontrado\"><code>docker compose restart</code> dijo \"no se proporcionó archivo de configuración: no encontrado\"<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/#docker-compose-restart-dijo-no-se-proporcion%C3%B3-archivo-de-configuraci%C3%B3n-no-encontrado\" class=\"hash-link\" aria-label=\"Enlace directo al docker-compose-restart-dijo-no-se-proporcionó-archivo-de-configuración-no-encontrado\" title=\"Enlace directo al docker-compose-restart-dijo-no-se-proporcionó-archivo-de-configuración-no-encontrado\" translate=\"no\">​</a></h3>\n<p>No estás en el directorio del proyecto que Compose espera — solo encuentra <code>docker-compose.yml</code> relativo a tu directorio de trabajo actual (o a través de <code>-f</code>). Primero, <code>cd</code> en la carpeta de la pila; este error exacto significa que Compose nunca miró siquiera a tus contenedores.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-trabajador-sigue-dando-errores-después-de-reiniciar-redis\">El trabajador sigue dando errores después de reiniciar Redis<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/#el-trabajador-sigue-dando-errores-despu%C3%A9s-de-reiniciar-redis\" class=\"hash-link\" aria-label=\"Enlace directo al El trabajador sigue dando errores después de reiniciar Redis\" title=\"Enlace directo al El trabajador sigue dando errores después de reiniciar Redis\" translate=\"no\">​</a></h3>\n<p>Reiniciar la <em>dependencia</em> no ayuda — la conexión que se rompió pertenece al servicio <em>dependiente</em>. Reinicia la cosa que mantiene la conexión (aquí, <code>langfuse-worker</code>), no la cosa a la que se conecta.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"docker-exec-container-id-username-devuelve-no-existe-tal-usuario\"><code>docker exec &lt;container&gt; id &lt;username&gt;</code> devuelve \"no existe tal usuario\"<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/#docker-exec-container-id-username-devuelve-no-existe-tal-usuario\" class=\"hash-link\" aria-label=\"Enlace directo al docker-exec-container-id-username-devuelve-no-existe-tal-usuario\" title=\"Enlace directo al docker-exec-container-id-username-devuelve-no-existe-tal-usuario\" translate=\"no\">​</a></h3>\n<p>La imagen no incluye un usuario no root dedicado bajo ese nombre — consulta la documentación de la imagen, o <code>docker exec &lt;container&gt; cat /etc/passwd</code> para ver qué está realmente disponible antes de elegir un 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=\"qué-sigue\">¿Qué sigue?<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al ¿Qué sigue?\" title=\"Enlace directo al ¿Qué sigue?\" translate=\"no\">​</a></h2>\n<p>El privilegio es la segunda capa: el endurecimiento de la red evita que un compromiso se propague entre pilas, el no root evita que un compromiso posea todo el contenedor. La última capa en esta serie es la identidad — poner autenticación real frente a los puertos que deben permanecer expuestos, que es donde <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/\">NetBird</a> y Caddy entran en juego.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lectura-adicional\">Lectura adicional<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/#lectura-adicional\" class=\"hash-link\" aria-label=\"Enlace directo al Lectura adicional\" title=\"Enlace directo al Lectura adicional\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.docker.com/reference/dockerfile/#user\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Documentación de Docker — especificar un usuario para ejecutar el contenedor</a></li>\n<li class=\"\"><a href=\"https://hub.docker.com/_/postgres\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Imagen de Docker de PostgreSQL — ejecutándose como un usuario no root</a></li>\n<li class=\"\"><a href=\"https://github.com/redis/ioredis#connection-events\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">ioredis — comportamiento de conexión y reconexión</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/",
            "title": "Root por defecto: endurecimiento del privilegio del contenedor en una pila de IA autoalojada",
            "summary": "Dos de los seis contenedores detrás de un despliegue en vivo de Langfuse estaban ejecutándose como root, sin razón aparente. Arreglarlo tomó una línea cada uno — y rompió un servicio que no tenía nada que ver con la solución. Una verdadera conversión, un verdadero fallo de coordinación, y cómo detectar ambos.",
            "date_modified": "2026-08-10T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "docker",
                "seguridad",
                "contenedores",
                "autoalojamiento",
                "agentes",
                "endurecimiento"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/",
            "content_html": "<div class=\"tutorialHero\"><svg viewBox=\"0 0 24 24\" fill=\"#2496ED\" aria-label=\"Docker\" class=\"tutorialHero__docker\"><path d=\"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z\"></path></svg><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">1</span><span class=\"skillTracker__skill\" data-state=\"current\">Lock down Docker networks</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-container-privilege-non-root/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run containers as non-root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sso-langfuse-authentik-oidc/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/authentik-groups-gateway-access-control/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Membership, not just an account</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/oauth2-client-credentials-agent-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">An identity for the agent, not a key</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/mcp-server-jwt-auth/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">A tool server that checks who is asking</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/agent-egress-firewall-docker/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Where the agent can go, not just what it can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rootless-docker-agent-host/\"><span class=\"skillTracker__dot\" data-state=\"locked\">8</span><span class=\"skillTracker__skill\" data-state=\"locked\">Move the daemon off root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/sandbox-untrusted-agent-code/\"><span class=\"skillTracker__dot\" data-state=\"locked\">9</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run the model's own code without trusting it</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/per-tool-scopes-mcp-challenge/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">A scope per tool, and a refusal clients can act on</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>Comienzas con un agente. Luego necesita una base de datos. Después agregas un segundo agente, un puente de mensajería, una pila de observabilidad. Seis meses después, una única instancia de WEC está ejecutando cinco proyectos de compose, veinte y tantos contenedores, y nadie recuerda qué puertos están abiertos al mundo.</p>\n<p>Eso no es un hipotético — esa es la caja en la que se escribió este tutorial. Así que en lugar de teorizar, lo investigamos: ¿pueden los contenedores alcanzarse entre sí a través de pilas? ¿Pueden alcanzar las bases de datos? ¿Está el firewall realmente protegiendo algo?</p>\n<p>Tres de las respuestas me sorprendieron. Una de ellas fue una base de datos sentada allí sin contraseña. Y el firewall — el firewall estaba mintiendo.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-caja-que-estamos-auditando\">La caja que estamos auditando<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/#la-caja-que-estamos-auditando\" class=\"hash-link\" aria-label=\"Enlace directo al La caja que estamos auditando\" title=\"Enlace directo al La caja que estamos auditando\" translate=\"no\">​</a></h2>\n<p>Cinco proyectos de compose independientes, desplegados durante meses, cada uno con su propia red (<code>host</code>, <code>none</code> y <code>bridge</code> son los integrados de Docker, más uno sobrante de un servicio que no está en ejecución):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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>Y aquí está lo que se publica al mundo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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=\"La auditoría de exposición — nueve redes listadas, cuatro servicios vinculados a cada interfaz\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 1.</strong> El inicio de cualquier auditoría. Nueve redes: tres integradas de Docker (<code>bridge</code>, <code>host</code>, <code>none</code>), cinco proyectos de compose, y uno sobrante. Cuatro servicios publicados en <code>0.0.0.0</code> — cada interfaz, incluida la pública.</p>\n<p>Cuatro servicios escuchando en cada interfaz. Nota lo que <em>no</em> está en esa lista: los Postgres, ClickHouse y Redis detrás de Langfuse. Esos fueron publicados en <code>127.0.0.1</code> en su lugar — alguien tomó una buena decisión allí. Mantén ese pensamiento.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prueba-1--aislan-realmente-las-redes-docker-separadas-sí-y-la-mayoría-de-los-blogs-están-equivocados\">Prueba 1 — ¿Aislan realmente las redes Docker separadas? (sí, y la mayoría de los blogs están equivocados)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/#prueba-1--aislan-realmente-las-redes-docker-separadas-s%C3%AD-y-la-mayor%C3%ADa-de-los-blogs-est%C3%A1n-equivocados\" class=\"hash-link\" aria-label=\"Enlace directo al Prueba 1 — ¿Aislan realmente las redes Docker separadas? (sí, y la mayoría de los blogs están equivocados)\" title=\"Enlace directo al Prueba 1 — ¿Aislan realmente las redes Docker separadas? (sí, y la mayoría de los blogs están equivocados)\" translate=\"no\">​</a></h2>\n<p>La preocupación clásica: OpenClaw es comprometido, y el atacante entra en la base de datos de Langfuse al lado. Vamos a averiguarlo en lugar de adivinar. Obtén las IPs de los contenedores:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash 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\">Salida</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>Ahora intenta alcanzar ambos <strong>desde dentro del contenedor de OpenClaw</strong>, por IP — sin DNS, sin atajos:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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(`ALCANZADO   ${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(`bloqueado   ${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\">Salida</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\">bloqueado   langfuse-postgres</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">bloqueado   evolution-postgres</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Las sondas entre redes se agotan desde dentro del contenedor de OpenClaw\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 2.</strong> Conectando por IP cruda desde el contenedor de una pila a la base de datos de otra pila: ambos se agotan. Las reglas de aislamiento de Docker están haciendo su trabajo.</p>\n<p><strong>Bloqueado.</strong> Las instalaciones modernas de Docker instalan reglas de <code>DOCKER-ISOLATION-STAGE</code> que eliminan el tráfico entre redes de puente definidas por el usuario, y funcionan. Si has leído que \"las redes Docker no realmente aíslan\", pruébalo en tu propia caja antes de creerlo — en Docker actual, sí lo hacen.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes verificar el aislamiento de red empíricamente en lugar de confiar en el folclore: obtén la IP del objetivo con <code>docker inspect</code>, luego conecta <strong>por IP</strong> desde dentro de otro contenedor.</p></div></div>\n<p>Así que el aislamiento entre pilas está bien. Lo que significa que el verdadero riesgo está en otro lugar.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prueba-2--dentro-de-una-red-todo-está-completamente-abierto\">Prueba 2 — Dentro de una red, todo está completamente abierto<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/#prueba-2--dentro-de-una-red-todo-est%C3%A1-completamente-abierto\" class=\"hash-link\" aria-label=\"Enlace directo al Prueba 2 — Dentro de una red, todo está completamente abierto\" title=\"Enlace directo al Prueba 2 — Dentro de una red, todo está completamente abierto\" translate=\"no\">​</a></h2>\n<p>Cada proyecto de compose coloca todos sus servicios en una red. Esa es la predeterminada, y significa que el contenedor de la aplicación puede alcanzar cada servicio de respaldo — lo cual es <em>intencionado</em>. La pregunta es cómo se ve realmente ese acceso. Desde el contenedor de puente de Evolution API:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\"ALCANZADO   {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\"bloqueado   {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\">Salida</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\">ALCANZADO   postgres:5432</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ALCANZADO   redis:6379</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ALCANZADO   api:8080</span><br></div></code></pre></div></div>\n<p>Alcanzable es esperado. <strong>No autenticado no lo es.</strong> Verifica si esos servicios realmente piden credenciales — primero 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\">Salida</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 respondiendo INFO sin AUTH\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 3.</strong> El hallazgo: <code>INFO server</code> respondió sin credenciales (salida truncada a 120 caracteres por la sonda). Cualquier contenedor en esta red puede leer o borrar el almacén de sesiones.</p>\n<p>Eso es un <strong>comando <code>INFO</code> respondido sin <code>AUTH</code></strong> — el Redis que mantiene el estado de sesión de este despliegue no tiene contraseña. Cualquier cosa que obtenga ejecución de código en <em>cualquier</em> contenedor en esa red puede leerlo, escribir en él, o <code>FLUSHALL</code> en él.</p>\n<p>Postgres, en la misma pila, se comporta correctamente:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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>Exige SCRAM-SHA-256. La misma caja, la misma red, dos servicios de respaldo — uno pide una contraseña, el otro no. Nadie decidió eso; es simplemente lo que las imágenes predeterminan cuando no estableces una contraseña.</p>\n<p><strong>La lección: \"red interna\" no es un límite de seguridad.</strong> Es una conveniencia. Trata cada servicio como si el atacante ya estuviera en esa red, porque si un contenedor cae, ellos están.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prueba-3--el-firewall-que-no-es\">Prueba 3 — El firewall que no es<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/#prueba-3--el-firewall-que-no-es\" class=\"hash-link\" aria-label=\"Enlace directo al Prueba 3 — El firewall que no es\" title=\"Enlace directo al Prueba 3 — El firewall que no es\" translate=\"no\">​</a></h2>\n<p>Ahora lo grande. Activa UFW y deniega todo lo entrante excepto 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\"># SIEMPRE primero, o te bloqueas</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\">Salida</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>Denegar entrante — solo SSH y el rango UDP de mosh permitido. Ahora, desde una <strong>máquina diferente</strong>, abre la interfaz de usuario de Langfuse y la API de Evolution:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Desde una laptop, no el servidor</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;  La interfaz de usuario de Langfuse se carga normalmente</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">http://&lt;instance-ip&gt;:8080   -&gt;  {\"status\":200,\"message\":\"Bienvenido a la API de Evolution...\"}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La API de Evolution respondiendo en un navegador mientras UFW deniega todo el tráfico entrante\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 4.</strong> El firewall dice que deniega todo lo entrante. La API de Evolution en <code>:8080</code> responde de todos modos — estado 200, directamente desde un navegador en otra máquina.</p>\n<p>Ambos completamente abiertos, a través de un firewall configurado para bloquearlos. Esta es la sorpresa de seguridad más común en Docker auto-alojado, y no es un error.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"por-qué-docker-llega-al-paquete-primero\">Por qué: Docker llega al paquete primero<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/#por-qu%C3%A9-docker-llega-al-paquete-primero\" class=\"hash-link\" aria-label=\"Enlace directo al Por qué: Docker llega al paquete primero\" title=\"Enlace directo al Por qué: Docker llega al paquete primero\" translate=\"no\">​</a></h3>\n<p>Mira el orden de la cadena <code>FORWARD</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\">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\">Salida</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 activo con denegar entrante, y las cadenas de Docker delante de las de UFW en FORWARD\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 5.</strong> La explicación completa en una pantalla: el firewall dice <em>denegar entrante</em>, y las cadenas de Docker están en las posiciones 1-2 mientras que las de UFW comienzan en 5.</p>\n<p>Las cadenas de Docker están en las posiciones <strong>1 y 2</strong>. Las de UFW no aparecen hasta <strong>5 y 6</strong>. Y publicar un puerto no es un oyente en el host — es una regla de NAT:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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>El paquete llega, el NAT de Docker reescribe el destino al contenedor, y se convierte en tráfico <em>reenviado</em> que la propia cadena de Docker acepta — mucho antes de que se consulte cualquier regla de UFW. Tus reglas de UFW no son ignoradas; simplemente nunca se alcanzan.</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>Esto es lo que debes recordar</div><div class=\"admonitionContent_BuS1\"><p><code>ufw deny</code> <strong>no</strong> protege un puerto Docker publicado. Si has estado confiando en UFW frente a <code>-p 8080:8080</code>, ese puerto ha estado abierto todo el tiempo.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"cuatro-soluciones-en-orden-de-preferencia\">Cuatro soluciones, en orden de preferencia<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/#cuatro-soluciones-en-orden-de-preferencia\" class=\"hash-link\" aria-label=\"Enlace directo al Cuatro soluciones, en orden de preferencia\" title=\"Enlace directo al Cuatro soluciones, en orden de preferencia\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-1--no-lo-publiques-en-absoluto\">Solución 1 — No lo publiques en absoluto<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/#soluci%C3%B3n-1--no-lo-publiques-en-absoluto\" class=\"hash-link\" aria-label=\"Enlace directo al Solución 1 — No lo publiques en absoluto\" title=\"Enlace directo al Solución 1 — No lo publiques en absoluto\" translate=\"no\">​</a></h3>\n<p>La solución más limpia no es una regla de firewall; es no abrir la puerta. Si un servicio solo es consumido por otros contenedores, no necesita <strong>ninguna</strong> entrada <code>ports:</code> — el tráfico de servicio a servicio funciona a través de la red de compose por nombre.</p>\n<p>Si necesitas que sea accesible desde el host (un puerto de depuración, un psql local), enlázalo explícitamente al loopback:</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\"># prefijo 127.0.0.1 = solo host, nunca la interfaz pública</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>Eso es exactamente lo que la pila de Langfuse en esta caja ya hace — su Postgres, ClickHouse y Redis están todos en <code>127.0.0.1</code>, que es por qué nunca aparecieron en nuestra auditoría de exposición. El predeterminado <code>\"5433:5432\"</code> significa <code>0.0.0.0</code> — cada interfaz, incluida la pública.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-2--divide-las-redes-por-zona-de-confianza-y-marca-los-backends-como-internal\">Solución 2 — Divide las redes por zona de confianza y marca los backends como <code>internal</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/#soluci%C3%B3n-2--divide-las-redes-por-zona-de-confianza-y-marca-los-backends-como-internal\" class=\"hash-link\" aria-label=\"Enlace directo al solución-2--divide-las-redes-por-zona-de-confianza-y-marca-los-backends-como-internal\" title=\"Enlace directo al solución-2--divide-las-redes-por-zona-de-confianza-y-marca-los-backends-como-internal\" translate=\"no\">​</a></h3>\n<p>Una red por proyecto de compose es la predeterminada, no un diseño. Da a los servicios de respaldo su propia red, y coloca solo la aplicación en ambas:</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\"># habla con el mundo Y con la base de datos</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\"># solo backend — sin ruta hacia afuera</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\"># por ejemplo, un receptor de webhook</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\"># no puede ver la base de datos en absoluto</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\"># sin gateway: sin entrada, sin salida</span><br></div></code></pre></div></div>\n<p>Verificado en la misma caja:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash 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\"># el contenedor solo de frontend intenta alcanzar la base de datos</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(\"ALCANZADO db  &lt;- fuga\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">except Exception as e: print(f\"bloqueado: {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\"># la app, que está en ambas redes</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(\"ALCANZADO db  &lt;- correcto, lo necesita\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">except Exception as e: print(f\"bloqueado: {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\"># y puede la base de datos alcanzar 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\">\"resuelto DNS externo\"</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\">\"sin salida a Internet  &lt;- interno funcionando\"</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\">Salida</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\">bloqueado: gaierror</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ALCANZADO db  &lt;- correcto, lo necesita</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sin salida a Internet  &lt;- interno funcionando</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La demostración de red interna: público bloqueado, app permitida, base de datos sin salida\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 6.</strong> Tres propiedades probadas en una ejecución — el contenedor de cara pública ni siquiera puede resolver la base de datos, la app puede alcanzarla, y la base de datos no tiene ruta a Internet.</p>\n<p>Tres propiedades a la vez: el contenedor de cara pública ni siquiera puede resolver la base de datos, la app puede, y la base de datos no puede comunicarse — lo cual importa el día que una dependencia tuya se vuelva maliciosa.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-3--autentica-todo-incluidos-los-servicios-internos\">Solución 3 — Autentica todo, incluidos los servicios \"internos\"<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/#soluci%C3%B3n-3--autentica-todo-incluidos-los-servicios-internos\" class=\"hash-link\" aria-label=\"Enlace directo al Solución 3 — Autentica todo, incluidos los servicios &quot;internos&quot;\" title=\"Enlace directo al Solución 3 — Autentica todo, incluidos los servicios &quot;internos&quot;\" translate=\"no\">​</a></h3>\n<p>Nuestra auditoría encontró a Redis respondiendo <code>INFO</code> sin credenciales. Una línea lo soluciona:</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\">Salida (después)</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>Esa es la salida real de esta caja, después de aplicar la solución — el hallazgo de la Prueba 2 está cerrado. Si estableces una contraseña en un Redis que algo ya usa, recuerda actualizar la cadena de conexión del cliente también (<code>redis://:&lt;password&gt;@host:6379/0</code>), o cambiarás una base de datos abierta por una rota.</p>\n<p>Haz esto incluso para servicios que no están publicados. La defensa en profundidad significa que la segunda capa se mantiene cuando la primera falla.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-4--cuando-debes-publicar-filtra-en-docker-user\">Solución 4 — Cuando debes publicar, filtra en <code>DOCKER-USER</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/#soluci%C3%B3n-4--cuando-debes-publicar-filtra-en-docker-user\" class=\"hash-link\" aria-label=\"Enlace directo al solución-4--cuando-debes-publicar-filtra-en-docker-user\" title=\"Enlace directo al solución-4--cuando-debes-publicar-filtra-en-docker-user\" translate=\"no\">​</a></h3>\n<p>A veces un puerto realmente tiene que ser de cara pública pero restringido por origen. La cadena a usar es <code>DOCKER-USER</code> — Docker garantiza que se ejecute <strong>antes</strong> de sus propias reglas de aceptación, y nunca la sobrescribe:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash 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\"># solo 203.0.113.0/24 puede abrir nuevas conexiones al puerto 3000 del contenedor</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>Probado en vivo en el puerto de Langfuse, desde un navegador en otra máquina:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Antes de la regla</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;  La interfaz de usuario de Langfuse se carga</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\">Después de la regla</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=\"La misma URL agotándose después de la regla DOCKER-USER\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 7.</strong> Misma URL, mismo firewall, una regla en la cadena correcta — y el puerto está finalmente cerrado. Nota la propia sugerencia de Chrome: <em>\"Verificando el proxy y el firewall.\"</em> Esta vez el firewall realmente es la respuesta.</p>\n<p>Mientras tanto, la API de Evolution en <code>:8080</code>, que deliberadamente dejamos sola, siguió respondiendo. La regla es quirúrgica, y funciona exactamente donde UFW no pudo.</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>Haz que sobreviva a un reinicio</div><div class=\"admonitionContent_BuS1\"><p><code>iptables -I</code> no es persistente. Usa <code>iptables-persistent</code> (<code>netfilter-persistent save</code>), o coloca la regla en una pequeña unidad de systemd que se ejecute después de <code>docker.service</code>. Una regla de firewall que desaparece al reiniciar es peor que ninguna, porque pensarás que estás protegido.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes auditar y asegurar un host Docker multi-pila: prueba qué es alcanzable, enlaza o divide lo que no debería ser, autentica los backends, y filtra los puertos publicados en la única cadena que Docker respeta.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-auditoría-como-una-lista-de-verificación\">La auditoría, como una lista de verificación<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/#la-auditor%C3%ADa-como-una-lista-de-verificaci%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al La auditoría, como una lista de verificación\" title=\"Enlace directo al La auditoría, como una lista de verificación\" translate=\"no\">​</a></h2>\n<p>Ejecuta estos cuatro comandos en cualquier caja que poseas. Toman un minuto y te dicen más que cualquier diagrama de arquitectura:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash 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. ¿Qué está expuesto al mundo?</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. ¿Están tus reglas de firewall incluso en la ruta? (Las cadenas de Docker antes de ufw = no lo están)</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. ¿Qué puede alcanzar un contenedor comprometido en su propia red?</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('alcanzado')\"</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. ¿Ese servicio de respaldo pide una contraseña?</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=\"lo-que-encontramos-y-lo-que-costó-arreglar\">Lo que encontramos, y lo que costó arreglar<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/#lo-que-encontramos-y-lo-que-cost%C3%B3-arreglar\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que encontramos, y lo que costó arreglar\" title=\"Enlace directo al Lo que encontramos, y lo que costó arreglar\" translate=\"no\">​</a></h2>\n<table><thead><tr><th>Hallazgo</th><th>Severidad</th><th>Solución</th></tr></thead><tbody><tr><td>El aislamiento entre redes funciona</td><td>✅ ninguno</td><td>nada — Docker ya hace esto</td></tr><tr><td>Cada servicio en una red, mutuamente alcanzable</td><td>⚠️ medio</td><td>dividir por zona de confianza, <code>internal: true</code></td></tr><tr><td>Redis respondiendo sin <code>AUTH</code></td><td>🔴 alto</td><td>una línea: <code>--requirepass</code></td></tr><tr><td><code>ufw deny</code> no aplicado a puertos publicados</td><td>🔴 alto</td><td>enlazar a <code>127.0.0.1</code>, o regla <code>DOCKER-USER</code></td></tr><tr><td>Bases de datos de respaldo en <code>127.0.0.1</code> (Langfuse)</td><td>✅ ninguno</td><td>ya correcto — copia este patrón</td></tr></tbody></table>\n<p>Ninguna de las soluciones tomó más de una línea de YAML. La parte costosa fue <em>saber qué línea</em> — y la única manera de saberlo es sondear tu propia caja en lugar de confiar en el diagrama en tu cabeza.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-de-problemas\">Solución de problemas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/#soluci%C3%B3n-de-problemas\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas\" title=\"Enlace directo al Solución de problemas\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"agregué-una-regla-docker-user-y-nada-cambió\">Agregué una regla <code>DOCKER-USER</code> y nada cambió<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/#agregu%C3%A9-una-regla-docker-user-y-nada-cambi%C3%B3\" class=\"hash-link\" aria-label=\"Enlace directo al agregué-una-regla-docker-user-y-nada-cambió\" title=\"Enlace directo al agregué-una-regla-docker-user-y-nada-cambió\" translate=\"no\">​</a></h3>\n<p>Dos causas habituales. Primero, <code>--dport</code> debe ser el puerto del <strong>contenedor</strong>, no el publicado — el DNAT ya reescribió el destino para cuando <code>DOCKER-USER</code> ve el paquete (en nuestro caso <code>3000</code>, no <code>3001</code>). Segundo, las conexiones existentes no se ven afectadas: la coincidencia <code>--ctstate NEW</code> solo toca las nuevas, así que una pestaña de navegador ya abierta sigue funcionando.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"habilité-ufw-y-perdí-mi-sesión-ssh\">Habilité UFW y perdí mi sesión SSH<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/#habilit%C3%A9-ufw-y-perd%C3%AD-mi-sesi%C3%B3n-ssh\" class=\"hash-link\" aria-label=\"Enlace directo al Habilité UFW y perdí mi sesión SSH\" title=\"Enlace directo al Habilité UFW y perdí mi sesión SSH\" translate=\"no\">​</a></h3>\n<p><code>sudo ufw allow 22/tcp</code> <strong>antes de</strong> <code>ufw enable</code>, siempre. Si ya estás bloqueado, la mayoría de los proveedores te dan una consola serial/VNC — úsala para ejecutar <code>ufw disable</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"internal-true-rompió-la-instalación-de-paquetes-de-mi-contenedor\"><code>internal: true</code> rompió la instalación de paquetes de mi contenedor<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/#internal-true-rompi%C3%B3-la-instalaci%C3%B3n-de-paquetes-de-mi-contenedor\" class=\"hash-link\" aria-label=\"Enlace directo al internal-true-rompió-la-instalación-de-paquetes-de-mi-contenedor\" title=\"Enlace directo al internal-true-rompió-la-instalación-de-paquetes-de-mi-contenedor\" translate=\"no\">​</a></h3>\n<p>Eso es que está funcionando. Una red interna no tiene gateway, así que no hay salida en absoluto — sin <code>pip install</code>, sin DNS, sin llamar a una API externa. Los servicios de respaldo no deberían necesitar nada de eso; si un contenedor lo hace, también pertenece a la red de frontend.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"los-servicios-no-pueden-encontrarse-después-de-que-dividí-las-redes\">Los servicios no pueden encontrarse después de que dividí las redes<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/#los-servicios-no-pueden-encontrarse-despu%C3%A9s-de-que-divid%C3%AD-las-redes\" class=\"hash-link\" aria-label=\"Enlace directo al Los servicios no pueden encontrarse después de que dividí las redes\" title=\"Enlace directo al Los servicios no pueden encontrarse después de que dividí las redes\" translate=\"no\">​</a></h3>\n<p>El DNS de Compose solo resuelve nombres <strong>dentro de una red compartida</strong>. Si <code>app</code> y <code>db</code> están en diferentes redes sin nada en común, <code>db</code> no se resolverá. La app tiene que ser miembro de ambas, como en la Solución 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=\"qué-sigue\">¿Qué sigue?<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al ¿Qué sigue?\" title=\"Enlace directo al ¿Qué sigue?\" translate=\"no\">​</a></h2>\n<p>El endurecimiento de la red es la capa que evita que un contenedor comprometido se convierta en un host comprometido. Las siguientes capas, en el orden en que las haría: <strong>ejecutar contenedores como no-root</strong> (<code>user:</code> en compose) para que una ruptura comience con menos privilegios, y poner un <strong>proveedor de identidad frente a los puertos que debes exponer</strong> — una malla privada con\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/\">NetBird</a> más autenticación es la combinación natural con todo lo anterior.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lectura-adicional\">Lectura adicional<a href=\"https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/#lectura-adicional\" class=\"hash-link\" aria-label=\"Enlace directo al Lectura adicional\" title=\"Enlace directo al Lectura adicional\" 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=\"\">Documentación de Docker — filtrado de paquetes y firewalls</a></li>\n<li class=\"\"><a href=\"https://docs.docker.com/engine/network/drivers/bridge/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Documentación de Docker — opciones del controlador de red bridge</a></li>\n<li class=\"\"><a href=\"https://redis.io/docs/latest/operate/oss_and_stack/management/security/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Seguridad de Redis — autenticación</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/harden-docker-networks-multi-agent/",
            "title": "Tu firewall te está mintiendo: endurecimiento de redes Docker para sistemas multi-agente",
            "summary": "Una caja ejecutando cinco pilas de agentes, un firewall configurado para denegar todo, y servicios que aún responden desde Internet público. Investigamos un despliegue real, encontramos una base de datos sin contraseña, demostramos por qué UFW nunca ve el tráfico de Docker, y lo solucionamos de cuatro maneras — cada comando y resultado de una ejecución en vivo.",
            "date_modified": "2026-08-03T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "docker",
                "seguridad",
                "redes",
                "autoalojamiento",
                "agentes",
                "endurecimiento"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/telegram-on-hermes/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/hermesagent-c76bbe81610ae2e70b0950e0f96bec8a.png\" alt=\"Hermes Agent\"><span class=\"tutorialHero__plus\">+</span><svg viewBox=\"0 0 24 24\" fill=\"#26A5E4\" aria-label=\"Telegram\" class=\"tutorialHero__docker\"><path d=\"M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 2 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->2</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->2<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting Hermes</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/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>En <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/\">Parte 1</a> alojaste Hermes con\nmemoria persistente. Esta parte lo conecta al <strong>mismo bot de Telegram</strong> de la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/\">serie OpenClaw</a> — el chat que\ntus usuarios ya conocen sigue funcionando exactamente como antes, solo que con un agente diferente respondiendo detrás. No hay un nuevo bot que anunciar, ni un canal al que migrar a la gente.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>Continúa en la misma instancia WEC que la Parte 1. El gateway de Hermes ya está\ninstalado y funcionando como un servicio de usuario systemd — esta parte es configuración,\nno instalación. Cada comando y error a continuación es de la ejecución real.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"por-qué-mantener-el-mismo-bot\">Por qué mantener el mismo bot<a href=\"https://development-wec.wiline.com/docs/es/tutorials/telegram-on-hermes/#por-qu%C3%A9-mantener-el-mismo-bot\" class=\"hash-link\" aria-label=\"Enlace directo al Por qué mantener el mismo bot\" title=\"Enlace directo al Por qué mantener el mismo bot\" translate=\"no\">​</a></h2>\n<p>El autoalojamiento solo vale la pena si no te ata al agente con el que comenzaste. El bot en sí — su nombre, su historial de chat, las personas que ya hablan con él — es el activo. El agente detrás de él es reemplazable. Cambiar OpenClaw por Hermes en el mismo bot prueba eso: misma puerta de entrada de Telegram, mismo hilo de conversación, nuevo cerebro.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/telegram-on-hermes/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">Hermes funcionando desde <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/\">Parte 1</a>, gateway\ninstalado como un servicio systemd.</li>\n<li class=\"\">Un token de bot de Telegram. Reutiliza el de la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/\">tutorial de Telegram de OpenClaw</a> a través de\n<strong>@BotFather → /mybots → tu bot → API Token</strong> — o crea uno nuevo con\n<strong>/newbot</strong> si no tienes el antiguo a mano. De cualquier manera, los pasos a continuación\nson idénticos.</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--ejecutar-el-asistente-de-configuración\">Paso 1 — Ejecutar el asistente de configuración<a href=\"https://development-wec.wiline.com/docs/es/tutorials/telegram-on-hermes/#paso-1--ejecutar-el-asistente-de-configuraci%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Ejecutar el asistente de configuración\" title=\"Enlace directo al Paso 1 — Ejecutar el asistente de configuración\" 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>Selecciona Telegram de la lista de plataformas, luego elige cómo conectarlo:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Salida</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\">  ¿Cómo te gustaría crear tu bot de Telegram?</span><br></div><div class=\"token-line\" style=\"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] Automático (recomendado)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        Escanea un código QR → confirma en Telegram → listo.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        No se necesita copiar y pegar el 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\">    [2] Manual</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        Crea un bot a través de @BotFather tú mismo y pega el 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\">Opción [1/2] [1]: 2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  Crear un bot a través de @BotFather en Telegram</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Token de bot de Telegram: **********************************************</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">✓ Token de Telegram guardado</span><br></div></code></pre></div></div>\n<p>Toma la opción <strong>2</strong> — la opción 1 crea un bot completamente nuevo a través de un código QR, lo cual derrota el propósito si estás reutilizando uno existente.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--abrir-acceso-y-un-verdadero-problema\">Paso 2 — Abrir acceso (y un verdadero problema)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/telegram-on-hermes/#paso-2--abrir-acceso-y-un-verdadero-problema\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — Abrir acceso (y un verdadero problema)\" title=\"Enlace directo al Paso 2 — Abrir acceso (y un verdadero problema)\" translate=\"no\">​</a></h2>\n<p>A continuación, el asistente pregunta quiénes pueden usar el bot:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Salida</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\">🔒 Seguridad: Restringir quién puede usar tu bot</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">   Para encontrar tu ID de usuario de Telegram:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">   1. Envía un mensaje a @userinfobot en Telegram</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">   2. Te responderá con tu ID numérico (por ejemplo, 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\">IDs de usuario permitidos (separados por comas, deja vacío para acceso abierto):</span><br></div></code></pre></div></div>\n<p>Si no tienes tu ID a mano, déjalo en blanco y continúa — puedes restringir esto más tarde. Pero <strong>el aviso es engañoso</strong>: dejarlo vacío no abre el acceso por defecto. Los registros del gateway contaron la verdadera historia después del reinicio:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Salida</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 se han configurado listas de permitidos de usuarios. Todos los usuarios no autorizados</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">serán denegados. Establece GATEWAY_ALLOW_ALL_USERS=true en ~/.hermes/.env para permitir</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">acceso abierto, o configura listas de permitidos de plataforma (por ejemplo,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">TELEGRAM_ALLOWED_USERS=tu_id).</span><br></div></code></pre></div></div>\n<p>Una lista de permitidos vacía significa <strong>denegar a todos</strong>, no acceso abierto. Si deseas acceso abierto (bien para un bot personal, no para nada público), necesitas un paso más:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash 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>El asistente también pregunta por un <strong>Canal Principal</strong> — donde Hermes entrega resultados de cron\ny mensajes entre plataformas. Déjalo en blanco también; puedes configurarlo más tarde\ndesde dentro del chat con <code>/sethome</code>.</p>\n<p>Termina el asistente y deja que reinicie el gateway:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Salida</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\">¿Reiniciar el gateway para recoger cambios? [Y/n]: Y</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">✓ Servicio de usuario reiniciado</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal mostrando la ejecución completa del asistente — token guardado, la advertencia de la lista de permitidos, y el reinicio del gateway\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 1.</strong> Todo el asistente, de principio a fin — token, la advertencia de la lista de permitidos, y el reinicio.</p>\n<p>Si agregaste <code>GATEWAY_ALLOW_ALL_USERS</code> después de que el asistente ya reinició\nuna vez (como arriba), reinicia de nuevo para que recoja el cambio en <code>.env</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token 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=\"paso-3--verificar\">Paso 3 — Verificar<a href=\"https://development-wec.wiline.com/docs/es/tutorials/telegram-on-hermes/#paso-3--verificar\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — Verificar\" title=\"Enlace directo al Paso 3 — Verificar\" translate=\"no\">​</a></h2>\n<p>Envía un mensaje a tu bot. <strong>No a @BotFather</strong> — a tu bot, por el nombre de usuario que le diste\ncuando lo creaste. Es fácil confundirse: BotFather responde todo con\nsu propio menú de comandos fijo, así que si ves eso en lugar de una respuesta real,\nestás en el chat equivocado.</p>\n<p>El primer mensaje puede recibir un empujón del sistema en lugar de una respuesta de chat — eso es\nHermes notando que aún no hay un canal principal, no un fallo:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Salida</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 se ha establecido un canal principal para Telegram. Un canal principal es donde Hermes</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">entrega resultados de trabajos cron y mensajes entre plataformas.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Escribe /sethome para hacer de este chat tu canal principal, o ignora para omitir.</span><br></div></code></pre></div></div>\n<p>Envía otro mensaje y responde de verdad:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Chat de Telegram mostrando al bot respondiendo &amp;quot;¡Hola! ¿Cómo puedo ayudar?&amp;quot; a través de Hermes\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 2.</strong> El mismo bot de la serie OpenClaw, ahora respondiendo a través de Hermes.</p>\n<p>Pregúntale algo con profundidad real, y entrega una respuesta estructurada real\n— no solo una respuesta estándar:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Chat de Telegram mostrando al bot explicando la dispersión de Rayleigh en respuesta a &amp;quot;¿Por qué es el cielo azul?&amp;quot;\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 3.</strong> Mismo agente, una pregunta real — explicación completa, no una línea.</p>\n<p>Mismo bot, mismo historial de chat, nuevo agente.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-de-problemas-real\">Solución de problemas (real)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/telegram-on-hermes/#soluci%C3%B3n-de-problemas-real\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas (real)\" title=\"Enlace directo al Solución de problemas (real)\" translate=\"no\">​</a></h2>\n<p><strong>\"No se han configurado listas de permitidos de usuarios. Todos los usuarios no autorizados serán denegados.\"</strong> —\ndejar el aviso de la lista de permitidos vacío no significa acceso abierto; significa\ndenegar a todos. Establece <code>GATEWAY_ALLOW_ALL_USERS=true</code> en <code>~/.hermes/.env</code> y reinicia\nel gateway.</p>\n<p><strong>\"Conflicto de sondeo de Telegram... asegúrate de que solo una instancia de bot esté\nejecutándose\"</strong> — aparece durante unos segundos justo después de un reinicio, mientras\nlos servidores de Telegram terminan de liberar la sesión anterior. Intenta de nuevo y\nse limpia solo dentro de los 20 segundos; no se necesita acción a menos que siga\nrepitiéndose más allá del 5to intento, lo que significa que otro proceso realmente está sondeando\nel mismo token.</p>\n<p><strong>El bot no responde en absoluto</strong> — verifica que estás enviando un mensaje al bot mismo, no\na @BotFather. BotFather siempre responde con su propia lista de comandos independientemente de\nlo que le envíes.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-sigue\">Qué sigue<a href=\"https://development-wec.wiline.com/docs/es/tutorials/telegram-on-hermes/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al Qué sigue\" title=\"Enlace directo al Qué sigue\" translate=\"no\">​</a></h2>\n<p>El bot responde — lo siguiente es ponerlo en un horario: un informe matutino o un\nchequeo de salud del servicio entregado a este mismo chat, sin necesidad de sondeo.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Telegram on Hermes</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/telegram-on-hermes/",
            "title": "Migrar el Bot de Telegram de OpenClaw a Hermes",
            "summary": "Mover el mismo bot de Telegram de la serie OpenClaw a Hermes — mismo chat, mismos usuarios, un agente diferente respondiendo detrás. Un verdadero asistente de configuración, un verdadero problema de lista de permitidos, y prueba de que responde.",
            "date_modified": "2026-07-29T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "autoalojamiento",
                "hermes",
                "telegram",
                "mensajería"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/web-search-wiline-inference/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/searxng-mark-1b536bb949960590cea598d56becda08.png\" alt=\"SearXNG\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 8 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->8</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->8<!-- --> earned</span></div><div class=\"skillTracker__series\">AI evals &amp; observability</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove a model works</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trustworthy JSON</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real test data at scale</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Observe &amp; score production</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">RAG, end to end</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">Catch regressions in CI</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/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>Supón que has construido un chatbot de soporte o un asistente RAG en WEC. Es sólido en tus documentos y\nen lo que el modelo ya sabe, pero pregúntale algo <em>actual</em> (\"¿cuál es la última\nversión de X?\", precios de hoy, un cambio reciente) y o bien adivina o te dice que su\nconocimiento está desactualizado. Para cualquier cosa que necesite estar al día, eso es una verdadera brecha.</p>\n<p>WEC Inference ahora tiene una <strong>herramienta de búsqueda web</strong> integrada. Agrégala a una llamada normal\n<code>/v1/chat/completions</code> y el modelo puede buscar cosas en la web en vivo: la búsqueda\nse realiza en la propia infraestructura de WiLine (SearXNG), por lo que nada se envía a un proveedor de búsqueda de terceros. Vamos a probarlo.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>Las llamadas llegan a <code>https://inference.wiline.com/v1/chat/completions</code> con tu clave API de WEC Inference,\nmodelo <code>Qwen3.5-9B</code>. No hay nada que instalar: la herramienta es parte de la API.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"sin-búsqueda-web\">Sin búsqueda web<a href=\"https://development-wec.wiline.com/docs/es/tutorials/web-search-wiline-inference/#sin-b%C3%BAsqueda-web\" class=\"hash-link\" aria-label=\"Enlace directo al Sin búsqueda web\" title=\"Enlace directo al Sin búsqueda web\" translate=\"no\">​</a></h2>\n<p>Una llamada normal, preguntando algo reciente:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\": \"¿Cuál es la última versión estable de Docker Engine, y cuándo fue lanzada?\"}]</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=\"El modelo respondiendo solo con datos de entrenamiento — una versión desactualizada\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 1.</strong> Sin herramienta, el modelo no se compromete a una versión — dice que su conocimiento está\nlimitado a su fecha de corte de entrenamiento y te señala las notas de la versión en su lugar.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"con-búsqueda-web\">Con búsqueda web<a href=\"https://development-wec.wiline.com/docs/es/tutorials/web-search-wiline-inference/#con-b%C3%BAsqueda-web\" class=\"hash-link\" aria-label=\"Enlace directo al Con búsqueda web\" title=\"Enlace directo al Con búsqueda web\" translate=\"no\">​</a></h2>\n<p>La <strong>misma</strong> solicitud, más una herramienta <code>web_search</code> y <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\": \"¿Cuál es la última versión estable de Docker Engine, y cuándo fue lanzada?\"}],</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\": \"Buscar en la web información actual\",</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\": \"La consulta de búsqueda\"}},</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=\"El modelo respondiendo con la versión actual, fundamentada en resultados web\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 2.</strong> Mismo modelo, misma pregunta, más la herramienta — ahora devuelve una versión concreta\ny fecha de lanzamiento obtenida de la web en vivo. El modelo decidió buscar, WEC ejecutó la consulta,\ny alimentó los resultados antes de responder.</p>\n<p>Esa es toda la función: una herramienta en el cuerpo de la solicitud, respuestas actuales saliendo.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"una-cosa-a-tener-en-cuenta-tokens\">Una cosa a tener en cuenta: tokens<a href=\"https://development-wec.wiline.com/docs/es/tutorials/web-search-wiline-inference/#una-cosa-a-tener-en-cuenta-tokens\" class=\"hash-link\" aria-label=\"Enlace directo al Una cosa a tener en cuenta: tokens\" title=\"Enlace directo al Una cosa a tener en cuenta: tokens\" translate=\"no\">​</a></h2>\n<p>La búsqueda web inyecta los resultados en tu aviso, por lo que una llamada con búsqueda cuesta más tokens que\nuna simple (en nuestra prueba, los tokens de aviso pasaron de ~26 a ~2,776). Mantén <code>tool_choice: auto</code>\npara que el modelo solo busque cuando realmente lo necesita — omitirá la búsqueda para preguntas que\nya puede responder.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"cuándo-usarlo\">Cuándo usarlo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/web-search-wiline-inference/#cu%C3%A1ndo-usarlo\" class=\"hash-link\" aria-label=\"Enlace directo al Cuándo usarlo\" title=\"Enlace directo al Cuándo usarlo\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong>Sí:</strong> versiones actuales, precios, noticias, cualquier cosa después de la fecha de corte de entrenamiento del modelo.</li>\n<li class=\"\"><strong>Omitir:</strong> conocimiento estable que el modelo ya tiene — <code>auto</code> se encarga de eso por ti.</li>\n</ul>\n<p>Dirígete a un asistente RAG o un chatbot y puede mantenerse actualizado sin que tengas que configurar un\nservicio de búsqueda separado.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Add live web search</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/web-search-wiline-inference/",
            "title": "Agrega búsqueda web a tus llamadas de WEC Inference",
            "summary": "Tu asistente RAG responde bien a partir de tus documentos, pero no sobre nada reciente. WEC Inference ahora tiene una herramienta de búsqueda web integrada: agrégala a una llamada de chat y el modelo obtiene información actual. Una prueba rápida antes/después.",
            "date_modified": "2026-07-23T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "inferencia",
                "web-search",
                "tool-use"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/",
            "content_html": "<div class=\"tutorialHero\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2255\" height=\"527\" fill=\"none\" viewBox=\"0 0 2255 527\" class=\"tutorialHero__langfuse\"><path fill=\"#1B1917\" d=\"M652.669 116.433c0-10.261-7.683-17.956-17.926-17.956H607V60h87.923v316.366h-42.254zM804.056 379.786c-39.693 0-72.131-27.362-72.131-68.831 0-41.042 28.596-69.686 84.935-69.686h39.693c7.256 0 12.805-5.558 12.805-12.826v-8.55c0-25.651-20.487-38.905-40.547-38.905-18.353 0-33.291 8.123-41.828 26.934h-45.668c11.95-44.462 46.095-65.41 88.349-65.41 40.547 0 82.374 22.231 82.374 77.808v156.046h-42.68v-14.109c0-5.13-5.549-7.695-9.817-4.702-16.646 11.97-32.438 22.231-55.485 22.231m5.975-38.477c18.78 0 34.572-9.406 52.498-27.362 4.695-4.702 6.829-10.688 6.829-17.1v-6.413c0-7.268-5.549-12.826-12.805-12.826H815.58c-27.743 0-40.974 12.398-40.974 31.637 0 17.956 12.377 32.064 35.425 32.064M961.855 376.366V147.642h42.255v19.666c0 5.13 6.4 6.84 10.24 2.992 13.23-12.825 31.59-27.788 60.61-27.788 36.71 0 70.85 23.086 70.85 75.671v158.183h-42.25V227.161c0-29.499-17.93-45.745-39.7-45.745-20.91 0-35 11.971-49.93 29.499-7.26 8.978-9.82 19.238-9.82 30.354v135.097zM1287.48 467c-52.5 0-85.79-24.796-95.61-63.273h46.1c7.25 15.818 20.06 25.651 45.67 25.651 34.14 0 55.48-20.093 55.48-65.838v-7.268c0-5.13-4.27-7.695-9.39-3.42-14.08 12.398-32.01 20.093-49.08 20.093-58.05 0-96.46-44.889-96.46-115.003 0-70.113 44.39-115.43 98.17-115.43 15.79 0 30.3 4.275 44.38 14.963 5.98 4.275 12.38.855 12.38-5.986v-3.847h42.26V363.54c0 72.678-43.97 103.46-93.9 103.46m-2.56-132.959c19.2 0 33.29-8.55 44.81-20.949 7.26-8.122 9.39-13.68 9.39-26.506v-61.563c0-12.826-2.13-20.948-10.67-29.071-9.39-8.978-22.62-14.964-39.69-14.964-35 0-61.89 29.072-61.89 76.954 0 47.883 24.76 76.099 58.05 76.099M1455.92 199.372c0-7.268-5.97-13.253-13.23-13.253h-32.44v-38.477h32.44c7.26 0 13.23-5.985 13.23-13.253v-5.986c0-45.744 23.48-68.403 69.15-68.403h29.02v38.477h-29.45c-17.5 0-26.46 9.833-26.46 29.926v5.986c0 7.268 5.97 13.253 13.23 13.253h42.68v38.477h-42.68c-7.26 0-13.23 5.985-13.23 13.253v176.994h-42.26zM1652.02 381.496c-35.85 0-69.14-23.086-69.14-75.671V147.642h42.25v150.06c0 29.499 17.07 44.889 37.13 44.889 21.77 0 35.85-11.97 50.79-29.499 7.26-8.977 9.82-19.238 9.82-30.354V147.642h42.25v228.724h-42.25V356.7c0-5.131-6.4-6.841-10.24-2.993-13.24 12.826-31.59 27.789-60.61 27.789M1893.57 381.496c-38.84 0-79.39-19.239-90.06-65.838h43.54c6.4 17.528 23.9 29.498 44.81 29.498 23.05 0 37.13-13.68 37.13-30.353 0-16.246-11.09-25.224-28.59-30.354l-36.28-10.261c-31.58-8.978-55.06-29.499-55.06-64.556 0-38.049 35.43-67.12 75.55-67.12 32.01 0 70.85 14.535 81.09 65.41h-40.55c-5.55-17.528-20.06-29.071-40.54-29.071-20.06 0-34.58 12.398-34.58 28.216 0 13.253 8.11 23.086 27.32 28.644l34.15 9.833c32.43 9.406 58.47 29.072 58.47 66.693 0 39.332-34.15 69.259-76.4 69.259M2098.54 381.496c-61.46 0-102.01-51.73-102.01-119.706s43.11-119.278 101.58-119.278c63.6 0 96.89 51.302 96.89 109.872v23.087h-144.26c-5.98 0-8.54 3.847-7.26 13.68 4.7 32.064 30.31 54.295 55.49 54.295 18.78 0 35-9.405 45.67-27.788h44.81c-16.22 40.187-49.94 65.838-90.91 65.838m43.11-141.51c6.83 0 9.39-3.42 7.68-14.108-4.69-26.506-24.33-45.317-51.22-45.317-25.6 0-47.37 18.811-54.2 45.745-2.56 9.833.85 13.68 6.83 13.68z\"></path><path fill=\"#FF5D5F\" d=\"m286.292 286.105 34.597 27.791s26.473-19.661 45.941-22.545c20.418-3.025 42.202 8.359 62.388 21.93 30.489 20.498 56.149 46.508 56.149 46.508l30.06-29.493s-82.879-89.795-148.597-81.672c-43.105 5.328-80.538 37.481-80.538 37.481\"></path><path fill=\"#4E9CFF\" d=\"M88.358 114.862 60 146.056s79.009 73.732 141.224 73.732c28.358 0 67.684-22.216 101.523-51.079 19.283-16.448 40.835-35.13 62.388-35.13 14.487 0 33.594 7.673 51.612 27.824 0 0 11.63-6.974 18.716-11.985 6.228-4.404 15.479-11.91 15.479-11.91-25.918-27.663-63.407-47.883-85.807-45.9-36.299.005-62.388 22.601-94.717 48.735s-45.94 36.907-69.194 36.907c-39.134 0-112.866-62.388-112.866-62.388M88.358 352.463 60 321.269s79.009-73.732 141.224-73.732c28.358 0 67.684 22.216 101.523 51.079 19.283 16.448 40.835 35.13 62.388 35.13 14.556 0 33.518-7.989 51.612-28.358 0 0 10.877 6.705 17.582 11.344 6.894 4.769 17.015 12.655 17.015 12.655-25.931 27.883-63.693 48.323-86.209 46.33-36.299-.005-57.851-19.24-90.179-45.374-32.329-26.133-50.478-40.268-73.732-40.268-39.134 0-112.866 62.388-112.866 62.388M458.142 185.149c-7.378 5.1-19.283 12.478-19.283 12.478s6.806 14.746 6.806 34.597-6.239 36.866-6.239 36.866 10.688 6.675 17.582 11.343c7.162 4.849 18.149 13.045 18.149 13.045s13.045-27.224 13.045-61.254-13.045-59.552-13.045-59.552-10.236 7.792-17.015 12.477\"></path><path fill=\"#FF5D5F\" d=\"m287.995 180.612 32.895-27.224s26.473 19.046 45.941 21.93c20.417 3.026 42.202-8.359 62.388-21.93 30.489-20.498 56.149-46.507 56.149-46.507l30.06 29.492s-82.879 89.795-148.597 81.672c-43.105-5.328-78.836-37.433-78.836-37.433M208.601 91c42.538 0 78.264 36.299 78.264 36.299s-9.941 7.832-16.448 13.045c-6.777 5.429-17.582 14.179-17.582 14.179s-18.711-19.851-44.234-19.851c-10.465 0-24.066 6.286-38.567 18.716-11.188 9.591-22.829 21.514-30.627 36.299-6.743 12.784-10.42 27.85-10.776 43.672-.446 19.873 6.597 40.704 18.149 57.283 7.743 11.112 16.983 19.474 26.657 26.657 12.555 9.322 25.648 15.881 35.164 15.881 10.166 0 19.306-3.533 26.09-6.806 10.776-6.239 19.278-13.612 19.278-13.612l33.463 27.791s-13.612 13.612-32.323 23.821c-12.091 5.963-27.632 11.91-46.508 11.91-18.862 0-40.767-10.022-61.254-25.522-13.244-10.021-26.225-21.895-36.298-36.299-16.51-23.607-25.017-52.328-24.96-81.104.057-29.136 9.451-57.993 26.094-81.672C138.273 117.657 176.86 91 208.601 91\"></path></svg><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 8 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->8</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->8<!-- --> earned</span></div><div class=\"skillTracker__series\">AI evals &amp; observability</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove a model works</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trustworthy JSON</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real test data at scale</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Observe &amp; score production</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">RAG, end to end</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">Catch regressions in CI</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">7</span><span class=\"skillTracker__skill\" data-state=\"current\">Trace &amp; debug agent tool calls</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/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>Un agente que llama a herramientas hace dos cosas muy diferentes: <strong>razona</strong> (\"Debería buscar en la documentación\") y <strong>actúa</strong> (realmente llama a la herramienta). Cuando algo sale mal, la primera pregunta siempre es <em>qué capa falló</em> — ¿pensó mal o se rompió la acción? Un registro plano no puede responder eso. Un <strong>trazado</strong> puede.</p>\n<p>En este tutorial, construirás un pequeño agente que llama a herramientas en <strong>WEC Inference</strong>, lo instrumentarás con <strong>Langfuse</strong> para que cada paso de razonamiento y cada llamada a la herramienta se conviertan en un nodo inspeccionable, y luego depurarás <strong>dos</strong> fallos reales del árbol de trazado — incluyendo el peor tipo: una respuesta incorrecta y confiada que nunca lanza un error. Cada comando, error y captura de pantalla a continuación proviene de una ejecución real.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>Construido en una instancia de WEC con Docker, contra <strong>WEC Inference</strong> (<code>Qwen3.5-122B</code>) y un <strong>Langfuse</strong> autoalojado del\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/\">tutorial de observabilidad</a>. Las herramientas del agente incluyen el servicio RAG del\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/\">tutorial de RAG</a> — así que esta pieza une toda la serie. Todas las llamadas al modelo permanecen en WEC; nada sale hacia una API de terceros.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-construirás\">Lo que construirás<a href=\"https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/#lo-que-construir%C3%A1s\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que construirás\" title=\"Enlace directo al Lo que construirás\" translate=\"no\">​</a></h2>\n<!-- -->\n<p>Las líneas punteadas son el punto: cada paso se informa a Langfuse, por lo que el <strong>razonamiento</strong> del agente y sus <strong>acciones</strong> se presentan como nodos separados e inspeccionables.</p>\n<p><strong>Requisitos previos:</strong> una instancia de WEC con Docker, una <strong>clave API de WEC Inference</strong>, un Langfuse en funcionamiento (clave pública + clave secreta, URL del host) y una herramienta que el agente pueda llamar — aquí el servicio RAG <code>/ask</code>. Todos los comandos se ejecutan en <code>~/agent-tracing</code>.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--ver-las-dos-capas-sin-trazado-aún\">Paso 1 — Ver las dos capas (sin trazado aún)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/#paso-1--ver-las-dos-capas-sin-trazado-a%C3%BAn\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Ver las dos capas (sin trazado aún)\" title=\"Enlace directo al Paso 1 — Ver las dos capas (sin trazado aún)\" translate=\"no\">​</a></h2>\n<p><strong>Regla: nunca instrumentes antes de haber visto el comportamiento en bruto.</strong> Así que v1 hace lo mínimo — pregunta al modelo una cuestión <em>con una definición de herramienta</em>, y muestra lo que regresa. Configura el espacio de trabajo y la clave de WEC:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">\"Buscar en la documentación de WEC para obtener una respuesta a una pregunta.\"</span><span class=\"token punctuation\" style=\"color:#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\">\"¿Cómo creo una instancia de cómputo en 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\">\"=== CAPA DE RAZONAMIENTO (lo que pensó) ===\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"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\">\"(ninguno)\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"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=== CAPA DE ACCIÓN (lo que decidió hacer) ===\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"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\"herramienta: </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>Ejecuta esto en un contenedor (mismo patrón que el resto de la serie):</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=\"El razonamiento del modelo y su elección de herramienta se imprimen como dos bloques separados\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 1.</strong> WEC Inference devuelve las dos capas <em>en una respuesta</em>: <code>reasoning_content</code>\n(el pensamiento) y <code>tool_calls</code> (la decisión). No tenemos que inferir la división — la API nos la proporciona.</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 admite llamadas a herramientas — y expone el razonamiento</div><div class=\"admonitionContent_BuS1\"><p><code>Qwen3.5-122B</code> devuelve <code>finish_reason: tool_calls</code> con un array <code>tool_calls</code> adecuado, <strong>más un campo <code>reasoning_content</code></strong> que contiene la cadena de pensamiento del modelo antes de actuar. Ese campo de razonamiento es exactamente lo que hace visible la división entre razonamiento y acción más adelante.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--cerrar-el-bucle-ejecutar-la-herramienta-obtener-la-respuesta\">Paso 2 — Cerrar el bucle: ejecutar la herramienta, obtener la respuesta<a href=\"https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/#paso-2--cerrar-el-bucle-ejecutar-la-herramienta-obtener-la-respuesta\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — Cerrar el bucle: ejecutar la herramienta, obtener la respuesta\" title=\"Enlace directo al Paso 2 — Cerrar el bucle: ejecutar la herramienta, obtener la respuesta\" translate=\"no\">​</a></h2>\n<p>v1 <em>decidió</em> buscar pero no lo hizo. Ahora llama realmente a la herramienta (el servicio RAG <code>/ask</code>), alimenta el resultado de vuelta y deja que el modelo escriba la respuesta final. Agrega la herramienta + el bucle:</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, adiciones)</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;tu-ip-vm&gt;:8000/ask\"</span><span class=\"token plain\">   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># el servicio RAG = nuestra única herramienta</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=== RESPUESTA FINAL ===\\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\"[razonamiento] </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\"[acción]    </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\"[resultado de la herramienta] </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=\"El bucle completo del agente: razonamiento, acción, resultado de la herramienta, luego la respuesta final fundamentada\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 2.</strong> El bucle completo — el modelo razona, llama a <code>search_docs</code>, obtiene el resultado de RAG y escribe una respuesta fundamentada. Funciona. Pero un terminal que funciona no te dice <em>nada</em> sobre si el razonamiento fue correcto — eso es lo que arreglamos a continuación.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--agregar-langfuse-las-capas-se-convierten-en-un-árbol-de-trazado\">Paso 3 — Agregar Langfuse: las capas se convierten en un árbol de trazado<a href=\"https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/#paso-3--agregar-langfuse-las-capas-se-convierten-en-un-%C3%A1rbol-de-trazado\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — Agregar Langfuse: las capas se convierten en un árbol de trazado\" title=\"Enlace directo al Paso 3 — Agregar Langfuse: las capas se convierten en un árbol de trazado\" translate=\"no\">​</a></h2>\n<p>El terminal aplana todo en un solo flujo. Langfuse convierte cada función en un <strong>nodo</strong> para que puedas ver la estructura. Reutiliza las claves de Langfuse del tutorial de observabilidad (mismo proyecto que tus trazas de RAG):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Agrega el SDK (<code>pip install ... langfuse</code>) y decora tres funciones — esa es toda la instrumentación. <code>@observe</code> envuelve una función como un span; <code>as_type=\"generation\"</code> marca las llamadas LLM para que Langfuse capture modelo, tokens y costo:</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, decoradores)</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\">\"¿Cómo creo una instancia de cómputo en 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\"># script de corta duración: empujar trazas antes de salir</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>Abre Langfuse → <strong>Trazado → Trazas</strong>. Lo primero que notarás: hay <strong>dos</strong> trazas por pregunta.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La lista de trazas de Langfuse mostrando una traza de ejecución y una traza de pregunta\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 3.</strong> Trazado distribuido, gratis: la traza <code>run</code> de tu agente y la propia traza <code>ask</code> del servicio RAG son <em>separadas</em> — cada servicio se instrumenta a sí mismo. Ver ambas es cómo sigues una solicitud a través de los límites del servicio.</p>\n<p>Abre la más nueva <code>run</code>:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El árbol de trazado de Langfuse: ejecución, dos generaciones de call_model y el span de search_docs\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 4.</strong> El turno del agente como un árbol: <code>run</code> → <code>call_model</code> (decidir) → <code>search_docs</code> (actuar) → <code>call_model</code> (respuesta). Langfuse también capturó <strong>latencia</strong>, <strong>uso de tokens</strong> (801→306) y una <strong>puntuación</strong> de corrección — todo por tres decoradores.</p>\n<p>Haz clic en el primer <code>call_model</code> y abre su salida:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El nodo call_model mostrando reasoning_content y tool_calls en Langfuse\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 5.</strong> La capa de razonamiento, capturada: <code>reasoning_content</code> (\"<em>Debería buscar en la documentación de WEC…</em>\") se encuentra justo al lado de los <code>tool_calls</code> que produjo. Este es el registro contra el que depuras.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--romperlo-de-la-manera-ruidosa-luego-depurar-desde-el-trazado\">Paso 4 — Romperlo (de la manera ruidosa), luego depurar desde el trazado<a href=\"https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/#paso-4--romperlo-de-la-manera-ruidosa-luego-depurar-desde-el-trazado\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — Romperlo (de la manera ruidosa), luego depurar desde el trazado\" title=\"Enlace directo al Paso 4 — Romperlo (de la manera ruidosa), luego depurar desde el trazado\" translate=\"no\">​</a></h2>\n<p>Planta un <strong>error realista</strong>: la API de RAG espera <code>{\"question\": ...}</code>, pero es natural enviar <code>{\"query\": ...}</code> porque el parámetro de la <em>herramienta</em> se llama <code>query</code>. Una clave incorrecta:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Se bloquea:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El terminal mostrando un rastreo de error HTTP 422\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 6.</strong> El terminal te da un <strong>rastreo de pila</strong> — te dice <em>dónde se rompió el código</em>. No te dice <strong>si el agente pensó correctamente</strong>. Para eso, ve al trazado.</p>\n<p>Abre la <code>run</code> fallida y haz clic en el span rojo <code>search_docs</code>:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El trazado roto: call_model verde, search_docs rojo con un error 422\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 7.</strong> Todo el diagnóstico en una imagen: <code>call_model</code> es <strong>verde</strong> (el razonamiento fue correcto — eligió correctamente buscar), <code>search_docs</code> es <strong>rojo</strong>, murió en <strong>0.04s</strong>, y el panel muestra el <code>422</code> más la carga útil exacta que envió. Un fallo instantáneo es un <em>rechazo</em>, no un tiempo de espera — apuntando directamente a una mala solicitud.</p>\n<p>Arregla la línea y confirma que está verde:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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=\"El trazado corregido: todos los nodos verdes, respuesta final producida\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 8.</strong> De vuelta a verde — <code>search_docs</code> tiene éxito y se produce la respuesta final. Diagnosticaste <em>y</em> verificaste la corrección desde el trazado.</p>\n<p>Ese fue el fallo <strong>ruidoso</strong> — se bloqueó, así que lo habrías atrapado eventualmente incluso sin un trazado. El peligroso es el siguiente.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5--una-segunda-herramienta-para-que-el-agente-tenga-que-elegir\">Paso 5 — Una segunda herramienta, para que el agente tenga que <em>elegir</em><a href=\"https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/#paso-5--una-segunda-herramienta-para-que-el-agente-tenga-que-elegir\" class=\"hash-link\" aria-label=\"Enlace directo al paso-5--una-segunda-herramienta-para-que-el-agente-tenga-que-elegir\" title=\"Enlace directo al paso-5--una-segunda-herramienta-para-que-el-agente-tenga-que-elegir\" translate=\"no\">​</a></h2>\n<p>Los agentes reales tienen más de una herramienta, y las decisiones interesantes ocurren cuando el modelo elige entre ellas. Agrega una herramienta <code>get_pricing</code> junto a <code>search_docs</code>, y enruta las llamadas a través de una <strong>tabla de despacho</strong> indexada por el nombre de la herramienta:</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, dos herramientas + despacho)</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\">\"Buscar en la documentación de WEC para preguntas sobre cómo hacer y configurar.\"</span><span class=\"token punctuation\" style=\"color:#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\">\"Obtener el precio de un recurso de WEC (por ejemplo, 'instancia de cómputo', 'almacenamiento en bloque', 'inferencia').\"</span><span class=\"token punctuation\" style=\"color:#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\">\"instancia de cómputo\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"$0.03 / hora-vCPU\"</span><span class=\"token punctuation\" style=\"color:#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\">\"almacenamiento en bloque\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"$0.10 / GB-mes\"</span><span class=\"token punctuation\" style=\"color:#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\">\"inferencia\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"$0.50 por 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 se encontró precio para '</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\"># en run(), despachar por la herramienta que el modelo realmente eligió:</span><span class=\"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\"[acción] </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>Haz una pregunta de <strong>precio</strong> (<code>run(\"¿Cuánto cuesta una instancia de cómputo en WEC?\")</code>) y ejecútala. El agente debería elegir <code>get_pricing</code>, no <code>search_docs</code>:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Trazado de Langfuse donde el agente eligió correctamente get_pricing\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 9.</strong> Selección genuina de herramientas: dado dos herramientas, el agente eligió <code>get_pricing</code> y respondió <strong>$0.03/vCPU-hora</strong>, Corrección 1.00. La elección en sí misma es ahora algo que puedes ver en el trazado.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-6--el-fallo-silencioso-por-qué-el-trazado-vale-la-pena\">Paso 6 — El fallo silencioso (por qué el trazado vale la pena)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/#paso-6--el-fallo-silencioso-por-qu%C3%A9-el-trazado-vale-la-pena\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 6 — El fallo silencioso (por qué el trazado vale la pena)\" title=\"Enlace directo al Paso 6 — El fallo silencioso (por qué el trazado vale la pena)\" translate=\"no\">​</a></h2>\n<p>Ahora un error que <strong>no se bloquea</strong>. Alguien refactoriza el despacho y codifica en duro la antigua herramienta única, olvidando la nueva — así que cada llamada ejecuta <code>search_docs</code> sin importar lo que eligió el modelo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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]))  # ERROR/'</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\">Una ejecución exitosa — con una respuesta completamente incorrecta</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': 'instancia de cómputo'})</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">=== RESPUESTA FINAL ===</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Basado en la información de precios disponible, la categoría de instancia de cómputo más cara en WEC es $1,586.77. ...</span><br></div></code></pre></div></div>\n<p>Lee eso cuidadosamente. El modelo <strong>eligió <code>get_pricing</code></strong> (correcto). La respuesta es confiada, detallada — y <strong>completamente incorrecta</strong> (el precio real es $0.03/vCPU-hora). No hay <strong>error, ni rastreo</strong>. Un registro se vería perfectamente saludable. Este es el modo de fallo que se envía a producción y miente silenciosamente a los usuarios.</p>\n<p>El trazado es lo único que lo atrapa. Abre la <code>run</code>:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El trazado de fallo silencioso: get_pricing elegido pero ejecutado search_docs, Corrección 0.00\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 10.</strong> Tres indicios que un registro no puede mostrar: (1) el span ejecutado es <strong><code>search_docs</code></strong> — no hay <strong>span de <code>get_pricing</code></strong>, a pesar de que el modelo lo eligió: <em>decisión ≠ acción</em>; (2) el nodo de razonamiento puntuó <strong>Corrección 0.00</strong> — el juez LLM marcó automáticamente la respuesta incorrecta; (3) la salida muestra al modelo <em>confundido</em> por un resultado de herramienta que no coincidía con lo que pidió. Arregla el despacho de vuelta a <code>TOOL_FUNCS[name](**args)</code> y el precio es correcto de nuevo.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Instrumentaste un agente que llama a herramientas para que su razonamiento y sus acciones sean nodos de trazado separados e inspeccionables — y depuraste <strong>dos</strong> fallos reales desde el trazado: el ruido del choque <em>y</em> la respuesta silenciosa, confiada pero incorrecta que nunca revelaría un rastreo de pila.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-de-problemas\">Solución de problemas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/#soluci%C3%B3n-de-problemas\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas\" title=\"Enlace directo al Solución de problemas\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong>No aparece trazado.</strong> Un script corto sale antes de que el SDK se vacíe. Llama a\n<code>get_client().flush()</code> antes de que el proceso termine (ya está en v3).</li>\n<li class=\"\"><strong>Span de <code>search_docs</code> faltante.</strong> Solo las funciones decoradas con <code>@observe</code> se convierten en nodos —\ndecora la función, no el sitio de llamada.</li>\n<li class=\"\"><strong>La generación no muestra tokens/costo.</strong> Pasa <code>usage_details=data.get(\"usage\")</code> a través de\n<code>update_current_generation</code>; sin ello, Langfuse no puede calcular el costo.</li>\n<li class=\"\"><strong>Las trazas del agente y de la herramienta parecen desconectadas.</strong> Esperado — cada servicio se traza a sí mismo. Conéctalos más tarde con propagación de trazas si necesitas una vista única de servicio cruzado.</li>\n<li class=\"\"><strong>Una respuesta incorrecta silenciosa sin error.</strong> Compara los <code>tool_calls</code> del modelo (lo que eligió)\ncontra los spans ejecutados (lo que realmente se ejecutó). Un desajuste es un error de despacho/ruteo — y una puntuación de corrección en el trazado lo marcará automáticamente.</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=\"qué-sigue\">Qué sigue<a href=\"https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al Qué sigue\" title=\"Enlace directo al Qué sigue\" translate=\"no\">​</a></h2>\n<p>Ahora puedes ver a un agente pensar, actuar y <em>fallar silenciosamente</em> — y atraparlo. Apunta la misma instrumentación a un agente real de múltiples pasos, agrega puntuación de corrección automatizada en cada trazado, y tendrás una observabilidad de producción que resalta fallos silenciosos antes de que lo hagan tus usuarios.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"desmontaje\">Desmontaje<a href=\"https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/#desmontaje\" class=\"hash-link\" aria-label=\"Enlace directo al Desmontaje\" title=\"Enlace directo al Desmontaje\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/agent-tracing </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose down </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rmi</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">local</span><br></div></code></pre></div></div>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/",
            "title": "Trazado a nivel de componente: depuración de llamadas a herramientas del agente",
            "summary": "El pensamiento de tu agente y sus acciones son dos capas diferentes. Construye un agente que llame a herramientas en WEC Inference, trázalo con Langfuse y luego depura dos fallos reales del trazado, incluyendo la respuesta incorrecta y confiada que nunca lanza un rastreo de pila.",
            "date_modified": "2026-07-22T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "evals",
                "observabilidad",
                "langfuse",
                "agentes",
                "trazado",
                "inferencia"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/whatsapp-ai-assistant-evolution-api/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/whatsapp_logo-cc34546fcff90d36128c7230c48c496f.png\" alt=\"WhatsApp\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__evolution\" src=\"https://development-wec.wiline.com/docs/es/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>La mayoría de las guías de \"autoalbergar un asistente de IA para WhatsApp\" se detienen en \"el contenedor se inició\". Esta va hasta el final: despliegas una puerta de enlace de WhatsApp programable real (<strong>API de Evolution</strong>), luego <strong>escribes el puente tú mismo</strong> — las ~50 líneas que convierten un mensaje entrante en una respuesta de LLM y la envían de vuelta. Ese puente (webhook → modelo → respuesta) es el patrón reutilizable detrás de <em>cada</em> integración de chat-AI: SMS, Slack, Telegram, voz — cambia el canal, la forma es idéntica.</p>\n<p>Y porque esto es una construcción real, encontramos — y solucionamos — cada detalle: una imagen que movió a los editores, un bucle de versión de Baileys, un bucle de respuesta infinito, spam en grupos de chat, la nueva dirección <strong>LID</strong> de WhatsApp, y una genuina <strong>pared de entrega</strong> que la mayoría de los tutoriales pretenden que no existe. Cada comando, error y salida a continuación es de una ejecución real.</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>Lee esto antes de usar tu número personal</div><div class=\"admonitionContent_BuS1\"><p>Evolution se vincula a un número de WhatsApp como un <strong>dispositivo compañero</strong> (como WhatsApp Web) — el bot actúa <em>como esa cuenta</em> y puede leer cada DM y grupo que recibe. Para una demostración en un número que controlas está bien; para cualquier cosa real, usa un <strong>número dedicado</strong>. Y consulta la <a href=\"https://development-wec.wiline.com/docs/es/tutorials/whatsapp-ai-assistant-evolution-api/#the-delivery-wall\" class=\"\">limitación de entrega</a> al final antes de construir esto en producción.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-construirás\">Lo que construirás<a href=\"https://development-wec.wiline.com/docs/es/tutorials/whatsapp-ai-assistant-evolution-api/#lo-que-construir%C3%A1s\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que construirás\" title=\"Enlace directo al Lo que construirás\" translate=\"no\">​</a></h2>\n<!-- -->\n<p>Cuatro contenedores: Evolution + su Postgres + Redis, y el <strong>puente</strong> que escribirás. El puente es el objetivo principal — todo lo demás es estándar.</p>\n<p><strong>Requisitos previos:</strong> una instancia WEC con Docker + Compose, una <strong>clave de API de Inferencia WEC</strong> (<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/cloud_portal/platform/inference/api_keys/\">Inferencia → Claves de API</a>), un número de WhatsApp para la demostración, y ~2 GB de espacio libre en disco.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--desplegar-la-pila-de-evolution\">Paso 1 — Desplegar la pila de Evolution<a href=\"https://development-wec.wiline.com/docs/es/tutorials/whatsapp-ai-assistant-evolution-api/#paso-1--desplegar-la-pila-de-evolution\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Desplegar la pila de Evolution\" title=\"Enlace directo al Paso 1 — Desplegar la pila de Evolution\" translate=\"no\">​</a></h2>\n<p>Evolution necesita su propio Postgres y Redis. Le damos un conjunto dedicado en una red interna y exponemos solo el puerto de la API (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>Rotación de logs desde el primer día</div><div class=\"admonitionContent_BuS1\"><p>El bloque <code>logging:</code> limita los logs del contenedor a 3×50 MB. Si lo omites, un contenedor ruidoso puede llenar el disco y hacer que la máquina se caiga — una lección aprendida de la manera difícil en esta VM. Configúralo antes de necesitarlo.</p></div></div>\n<p>El archivo <code>.env</code> (secretos generados, URIs de conexión y — lo más importante — una <strong>versión de WhatsApp-Web de Baileys fijada</strong>):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Generar el .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;tu-ip-vm&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>Levántalo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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\": \"¡Bienvenido a la API de Evolution, está funcionando!\",</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=\"La API de Evolution responde en el puerto 8080 con la carga de bienvenida\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 1.</strong> La pila está activa: Evolution responde en <code>:8080</code> con su versión y estado.</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>Dos problemas reales al extraer la imagen</div><div class=\"admonitionContent_BuS1\"><p><strong>(1) El editor se mudó.</strong> La imagen a la que la mayoría de las guías hacen referencia — <code>atendai/evolution-api</code> — ahora devuelve <em>\"acceso denegado al pull / el repositorio no existe.\"</em> La imagen actual es <strong><code>evoapicloud/evolution-api</code></strong>. La lista de etiquetas del antiguo repositorio aún se resuelve, lo que te envía por un camino sin salida; confirma con un pull fresco de <code>hello-world</code> que no está limitando la tasa.\n<strong>(2) Fija una etiqueta real.</strong> <code>v2.1.1</code> (de un blog popular) nunca fue publicada — verifica <code>https://hub.docker.com/v2/repositories/evoapicloud/evolution-api/tags</code> y fija una que exista (usamos <code>v2.2.3</code>).</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--crear-una-instancia-y-vincular-whatsapp\">Paso 2 — Crear una instancia y vincular WhatsApp<a href=\"https://development-wec.wiline.com/docs/es/tutorials/whatsapp-ai-assistant-evolution-api/#paso-2--crear-una-instancia-y-vincular-whatsapp\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — Crear una instancia y vincular WhatsApp\" title=\"Enlace directo al Paso 2 — Crear una instancia y vincular WhatsApp\" translate=\"no\">​</a></h2>\n<p>Establece tu clave de API como una variable de shell (cada solicitud la necesita en un encabezado <code>apikey:</code>):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">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>Crea la instancia:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Luego abre el Administrador integrado en <code>http://&lt;tu-ip-vm&gt;:8080/manager</code>, ingresa tu URL de servidor + clave de API, haz clic en <strong>wec-demo</strong>, y escanea el QR desde <strong>WhatsApp → Configuración → Dispositivos vinculados → Vincular un dispositivo</strong>. Confirma que está conectado:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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=\"El Administrador de Evolution muestra la instancia wec-demo conectada\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 2.</strong> El panel de control del Administrador de Evolution una vez que la instancia se vincula: <code>Conectado</code>, con conteos de contactos, chats y mensajes en vivo.</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>El bucle de versión de Baileys — el fallo número 1 de autoalbergado</div><div class=\"admonitionContent_BuS1\"><p>Si la instancia nunca sale de <code>conectando</code> y los logs muestran <code>ChannelStartupService</code> reinicializándose cada pocos segundos con una línea <code>Baileys version env: 2,3000,...</code>, la versión de WhatsApp-Web fijada está <strong>obsoleta</strong> y WhatsApp rechaza el apretón de manos antes de que se genere un QR. Obtén la versión actual y fíjala, luego recrea:</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=\"paso-3--el-puente-v1-ver-el-webhook\">Paso 3 — El puente, v1: <em>ver</em> el webhook<a href=\"https://development-wec.wiline.com/docs/es/tutorials/whatsapp-ai-assistant-evolution-api/#paso-3--el-puente-v1-ver-el-webhook\" class=\"hash-link\" aria-label=\"Enlace directo al paso-3--el-puente-v1-ver-el-webhook\" title=\"Enlace directo al paso-3--el-puente-v1-ver-el-webhook\" translate=\"no\">​</a></h2>\n<p>Ahora la parte que realmente escribes. <strong>Regla: nunca conectes la lógica antes de haber visto los datos reales.</strong> Así que v1 no hace nada más que imprimir lo que Evolution envía.</p>\n<p>El puente se ejecuta como otro contenedor en el mismo Compose, por lo que Evolution lo alcanza <strong>por nombre</strong> (<code>http://bridge:8090</code>) y el puente alcanza a Evolution en <code>http://evolution-api:8080</code>. Crea <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 RECIBIDO ===\"</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> es importante</div><div class=\"admonitionContent_BuS1\"><p>Sin él, la salida de <code>print</code> se almacena en búfer y nunca aparece en <code>docker compose logs</code> — estarás mirando un log vacío convencido de que está roto.</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>Agrega el servicio a <code>docker-compose.yml</code> (dentro de <code>services:</code>), luego constrúyelo:</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 (agregar bajo 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>Ahora <strong>registra el webhook</strong> para que Evolution envíe mensajes al puente:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Sigue los logs (<code>docker compose logs -f bridge</code>) y envíate un mensaje de WhatsApp. La carga real aparece:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Un webhook MESSAGES_UPSERT real</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': 'Contacto de Prueba',</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>Tres cosas que ahora <em>sabemos</em> — y cada una impulsa el código siguiente:</p>\n<ul>\n<li class=\"\"><strong>text</strong> está en <code>data.message.conversation</code> (plano) o <code>data.message.extendedTextMessage.text</code> (citado)</li>\n<li class=\"\"><strong><code>data.key.fromMe</code></strong> — <code>True</code> para nuestros propios mensajes (el guardia de bucle)</li>\n<li class=\"\"><strong><code>remoteJid</code> termina en <code>@lid</code></strong> — el identificador de privacidad de WhatsApp, <em>no</em> un número de teléfono (esta es una saga; Paso 5)</li>\n</ul>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--v2-leer-pensar-responder-con-el-guardia-de-bucle\">Paso 4 — v2: leer, pensar, responder (con el guardia de bucle)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/whatsapp-ai-assistant-evolution-api/#paso-4--v2-leer-pensar-responder-con-el-guardia-de-bucle\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — v2: leer, pensar, responder (con el guardia de bucle)\" title=\"Enlace directo al Paso 4 — v2: leer, pensar, responder (con el guardia de bucle)\" translate=\"no\">​</a></h2>\n<p>Dale al puente la clave de Inferencia WEC para que pueda llamar al modelo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash 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;tu-clave-de-inferencia-wec&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>Ahora la lógica — <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\"ENVIAR -&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) GUARDIA DE BUCLE</span><span class=\"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) extraer texto</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\"ENTRADA </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) pensar</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) responder</span><span class=\"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\"SALIDA </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>La única línea que los principiantes siempre pasan por alto es <strong>(1) el guardia de bucle</strong>. Cuando el puente envía una respuesta, ese mensaje saliente dispara <em>otro</em> webhook <code>MESSAGES_UPSERT</code> con <code>fromMe: true</code> — sin el guardia, el puente se responde a sí mismo, para siempre. Reconstruye:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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=\"El puente registrando ENTRADA y SALIDA para un DM en vivo\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 3.</strong> v2 en acción: un DM llega, el modelo responde, la respuesta se envía.</p>\n<p>Ahora responde a DMs. Pero observa lo que sucede a continuación.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5--las-dos-verdaderas-paredes-spam-en-grupos-luego-lid\">Paso 5 — Las dos verdaderas paredes: spam en grupos, luego LID<a href=\"https://development-wec.wiline.com/docs/es/tutorials/whatsapp-ai-assistant-evolution-api/#paso-5--las-dos-verdaderas-paredes-spam-en-grupos-luego-lid\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 5 — Las dos verdaderas paredes: spam en grupos, luego LID\" title=\"Enlace directo al Paso 5 — Las dos verdaderas paredes: spam en grupos, luego LID\" translate=\"no\">​</a></h2>\n<p>Sigue los logs con el bot en vivo y verás que intenta responder <strong>a cada grupo en el que estás</strong>, incluidos grupos de promoción/spam, y Evolution <strong>se agota</strong> al enviarles — errores de inundación:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Los grupos inundan el puente</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\">ENTRADA 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>Una cuenta compañera recibe <strong>todo</strong>. Solución: responder <strong>solo a DMs</strong> — los grupos terminan en <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\"># Solo DMs</span><span class=\"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>Ahora la pared más sutil. Con los grupos filtrados, un DM real llega — pero la respuesta es <strong>rechazada</strong>:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">ENVIAR -&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\":\"Contacto de Prueba\",\"number\":\"123456789012345@lid\"}]}}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Evolution rechazando una respuesta a una dirección @lid con exists\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 4.</strong> La pared LID en los logs: el DM entrante llega como <code>@lid</code>, y la respuesta es rechazada con <code>exists:false</code>.</p>\n<p><code>exists: false</code>. Esta es <strong>la dirección LID de WhatsApp</strong> (un cambio de privacidad de 2025): los DMs entrantes llegan con un identificador <code>@lid</code>, y <strong>no puedes enviar de vuelta a un <code>@lid</code></strong> — Evolution necesita el verdadero JID de teléfono <code>@s.whatsapp.net</code>. Sin embargo, el almacén de contactos de Evolution tiene ambos:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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==\"Contacto de Prueba\")]'</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\">Salida — la misma persona, dos identidades</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\": \"Contacto de Prueba\" },</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  { \"remoteJid\": \"5511999999999@s.whatsapp.net\", \"pushName\": \"Contacto de Prueba\" } ]</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"findContacts devolviendo el LID y el JID de teléfono para un contacto\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 5.</strong> Evolution almacena ambas identidades para la misma persona — el <code>@lid</code> y el verdadero JID de teléfono. Esa es la clave para resolver la dirección de respuesta.</p>\n<p>Así que <strong>resolvemos el <code>@lid</code> al número de teléfono</strong> antes de responder. Agrega un resolvedor y úsalo:</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: encontrar el número real del contacto haciendo coincidir el nombre</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>En el manejador, resuelve antes de enviar:</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>Ahora <code>ENVIAR -&gt; 201</code>. (Hacer coincidir por nombre es un heurístico — dos contactos que comparten un nombre colisionarían; la producción mantiene un mapa adecuado LID→teléfono. Pero funciona, y es el estado honesto del soporte LID de Evolution en v2.2.3.)</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-6--habla-whatsapp-no-markdown\">Paso 6 — Habla WhatsApp, no Markdown<a href=\"https://development-wec.wiline.com/docs/es/tutorials/whatsapp-ai-assistant-evolution-api/#paso-6--habla-whatsapp-no-markdown\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 6 — Habla WhatsApp, no Markdown\" title=\"Enlace directo al Paso 6 — Habla WhatsApp, no Markdown\" translate=\"no\">​</a></h2>\n<p>Los LLM emiten <strong>Markdown</strong> (<code>**negrita**</code>, <code>[texto](url)</code>), pero WhatsApp tiene su <em>propio</em> formato: la negrita es <code>*asterisco simple*</code>, y los enlaces de Markdown no se renderizan. Así que <code>**Compute**</code> aparece con asteriscos literales. Traduce la salida del modelo antes de enviar, y agrega las fuentes del RAG como URLs simples:</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\"># **negrita** -&gt; *negrita*</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\"># eliminar encabezados #</span><span class=\"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>Verificado contra una respuesta real — exactamente lo que WhatsApp renderizará:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Salida formateada (determinista, no se necesita entrega)</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\">Para crear una instancia de computación en WiLine Edge Cloud (WEC), sigue estos pasos:</span><br></div><div class=\"token-line\" style=\"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. Inicia sesión en WiLine Edge Cloud.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">2. En la barra lateral, haz clic en *Compute*.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">3. Selecciona *Instances*.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">4. Haz clic en el asistente \"Launch Virtual Machine\" para comenzar el proceso de despliegue.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">_Fuentes:_</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=\"La respuesta formateada de Markdown a WhatsApp\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 6.</strong> La salida del modelo después de to_whatsapp(): negrita con asterisco simple, enlaces simples, fuentes añadidas.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"mejora--apunta-el-puente-a-tu-documentación-rag\">Mejora — apunta el puente a tu documentación (RAG)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/whatsapp-ai-assistant-evolution-api/#mejora--apunta-el-puente-a-tu-documentaci%C3%B3n-rag\" class=\"hash-link\" aria-label=\"Enlace directo al Mejora — apunta el puente a tu documentación (RAG)\" title=\"Enlace directo al Mejora — apunta el puente a tu documentación (RAG)\" translate=\"no\">​</a></h2>\n<p>Todo hasta ahora usa el modelo en bruto, así que una pregunta de WEC obtiene una respuesta <em>general</em>. Para hacer que responda desde <strong>tu documentación</strong>, cambia <code>ask_llm</code> para llamar al servicio RAG de la <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/\">serie de evals</a> — <strong>una función</strong>, nada más cambia:</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;tu-ip-vm&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_Fuentes:_\\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>Ese es el beneficio del patrón de puente: <strong>cambia el cerebro, mantén la plomería.</strong> Ahora WhatsApp responde a preguntas de WEC desde la documentación real, con fuentes:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"WhatsApp entregando una respuesta fundamentada en documentos de WEC\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 7.</strong> Un mensaje real en → el puente → RAG sobre la documentación de WEC → una respuesta fundamentada entregada en WhatsApp, fuentes incluidas.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Has autoalbergado una puerta de enlace programable de WhatsApp y escribiste el puente que lo convierte en un asistente de IA — webhook entrante, modelo saliente, respuesta de vuelta — fundamentado en tu propia documentación.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-pared-de-entrega\">La pared de entrega<a href=\"https://development-wec.wiline.com/docs/es/tutorials/whatsapp-ai-assistant-evolution-api/#la-pared-de-entrega\" class=\"hash-link\" aria-label=\"Enlace directo al La pared de entrega\" title=\"Enlace directo al La pared de entrega\" translate=\"no\">​</a></h2>\n<p>Aquí está la parte que otros tutoriales no te dirán. Incluso con <code>ENVIAR -&gt; 201</code>, las respuestas a una <strong>cuenta migrada a LID</strong> frecuentemente llegan como <strong>\"Esperando este mensaje\"</strong> en el teléfono del destinatario — WhatsApp aceptó el texto cifrado pero ningún dispositivo puede descifrarlo. Es una limitación conocida del <strong>dispositivo compañero de Baileys</strong>, y el despliegue de LID de WhatsApp lo empeora.</p>\n<p>Lo que observamos, honestamente:</p>\n<ul>\n<li class=\"\">Un <strong>nuevo re-vinculo</strong> (cierra sesión en el dispositivo, escanea un nuevo QR) compra una <strong>ventana corta</strong> donde la entrega funciona sin problemas.</li>\n<li class=\"\">Después de un tiempo, la sesión se degrada y <code>\"Esperando este mensaje\"</code> regresa — aunque cada envío aún informa <code>201</code>.</li>\n</ul>\n<p>Así que el puente es correcto de extremo a extremo; el <em>transporte no oficial de Baileys</em> es el eslabón débil. Si estás construyendo esto para algo real:</p>\n<ul>\n<li class=\"\"><strong>Usa un número dedicado, no LID</strong> — el problema está vinculado a cuentas migradas a LID.</li>\n<li class=\"\">Para una fiabilidad de grado de producción, usa la <strong>API oficial de WhatsApp Cloud</strong>, o —\nsi tu objetivo es un agente de IA en lugar de una puerta de enlace programable — <strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/add-whatsapp-channel-openclaw/\">el canal nativo de WhatsApp de OpenClaw</a></strong>, que maneja la sesión por ti.</li>\n</ul>\n<p>Evolution brilla para <strong>automatización saliente a números que controlas</strong> (notificaciones, alertas, flujos). Como asistente de IA bidireccional en una cuenta personal de LID, trata la entrega como un esfuerzo mejor.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"resumen-de-solución-de-problemas\">Resumen de solución de problemas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/whatsapp-ai-assistant-evolution-api/#resumen-de-soluci%C3%B3n-de-problemas\" class=\"hash-link\" aria-label=\"Enlace directo al Resumen de solución de problemas\" title=\"Enlace directo al Resumen de solución de problemas\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong><code>acceso denegado al pull</code> en <code>atendai/evolution-api</code></strong> → imagen movida a <code>evoapicloud/evolution-api</code>.</li>\n<li class=\"\"><strong>Instancia atascada en <code>conectando</code>, bucle de reinicio</strong> → versión de Baileys obsoleta; fija <code>CONFIG_SESSION_PHONE_VERSION</code>.</li>\n<li class=\"\"><strong>El puente se responde a sí mismo para siempre</strong> → falta el guardia de bucle <code>fromMe</code>.</li>\n<li class=\"\"><strong>Inundación de mensajes de grupo / tiempos de espera de envío</strong> → filtra <code>@g.us</code>; responde solo a DMs.</li>\n<li class=\"\"><strong><code>ENVIAR -&gt; 400 exists:false ...@lid</code></strong> → resuelve el LID al JID de teléfono a través de <code>findContacts</code>.</li>\n<li class=\"\"><strong><code>**asteriscos**</code> en respuestas</strong> → traduce Markdown al formato de WhatsApp.</li>\n<li class=\"\"><strong>\"Esperando este mensaje\"</strong> → pared de cifrado de Baileys/LID; un nuevo re-vinculo es una ventana temporal; usa un número no LID / API Cloud para producción.</li>\n</ul>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"desmantelamiento\">Desmantelamiento<a href=\"https://development-wec.wiline.com/docs/es/tutorials/whatsapp-ai-assistant-evolution-api/#desmantelamiento\" class=\"hash-link\" aria-label=\"Enlace directo al Desmantelamiento\" title=\"Enlace directo al Desmantelamiento\" 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\"># agrega -v para también borrar la DB/sesión</span><br></div></code></pre></div></div>\n<p>Desvincula el dispositivo en <strong>WhatsApp → Dispositivos vinculados</strong> si has terminado.</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=\"qué-sigue\">¿Qué sigue?<a href=\"https://development-wec.wiline.com/docs/es/tutorials/whatsapp-ai-assistant-evolution-api/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al ¿Qué sigue?\" title=\"Enlace directo al ¿Qué sigue?\" translate=\"no\">​</a></h2>\n<p>Has construido el patrón de puente reutilizable — webhook → resolver → modelo → respuesta. Apúntalo a un cerebro diferente, a un canal diferente, o agrega herramientas. Si deseas un agente de IA en WhatsApp sin las advertencias de Baileys, el <strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/add-whatsapp-channel-openclaw/\">canal nativo de WhatsApp de OpenClaw</a></strong> es el camino gestionado; para respuestas fundamentadas en documentos, la <strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/\">serie de evals y observabilidad</a></strong> construye el servicio RAG en el que se conecta este tutorial.</p>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/whatsapp-ai-assistant-evolution-api/",
            "title": "Crea un asistente de IA para WhatsApp desde cero con Evolution y la API de WEC",
            "summary": "Autoalberga una puerta de enlace programable de WhatsApp (API de Evolution) y escribe el puente de ~50 líneas que lo convierte en un asistente de IA: webhook entrante, LLM saliente, respuesta de vuelta. Cada detalle es real: la imagen que movió a los editores, el bucle de la versión de Baileys, el guardia de bucle, el spam en grupos, la dirección LID de WhatsApp y la pared de entrega de la que nadie te advierte.",
            "date_modified": "2026-07-21T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "autoalojamiento",
                "whatsapp",
                "evolution-api",
                "docker",
                "inferencia"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/add-whatsapp-channel-openclaw/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/openclaw-wordmark-003352a1a7f02afc3bd877ae6f2dc175.png\" alt=\"OpenClaw\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/whatsapp_logo-cc34546fcff90d36128c7230c48c496f.png\" alt=\"WhatsApp\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 6 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->6</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->6<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting OpenClaw</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Deploy your own AI assistant</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real HTTPS + auth</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Chat from Telegram</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Private mesh access</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/run-openclaw-on-wec-models/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run it on WEC models</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">🏆</span><span class=\"skillTracker__skill\" data-state=\"current\">Chat from WhatsApp</span></span></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>Telegram (<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/\">Parte 3</a>) le dio a tu agente un <strong>bot</strong>.\nWhatsApp le da una <strong>línea telefónica</strong> — la aplicación que ~3 mil millones de personas ya usan, accesible sin fricción. Una advertencia que vale la pena entender desde el principio: WhatsApp no tiene cuenta de bot, así que OpenClaw se vincula a un número real como un <strong>dispositivo compañero</strong> (como WhatsApp Web) y el agente actúa <em>como esa cuenta</em>. Cada comando y error a continuación proviene de una ejecución real.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>Continúa desde las Partes 1–4 —\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/\">desplegar</a>,\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/\">HTTPS</a>,\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/\">Telegram</a>,\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/\">NetBird</a> — en OpenClaw <strong>2026.7.1</strong>. WhatsApp se vincula\npor <strong>QR</strong>, sin necesidad de webhook o dominio público.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-construirás\">Lo que construirás<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-whatsapp-channel-openclaw/#lo-que-construir%C3%A1s\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que construirás\" title=\"Enlace directo al Lo que construirás\" translate=\"no\">​</a></h2>\n<p>Cualquiera envía un mensaje al número; la puerta de enlace — vinculada como un dispositivo compañero — lo procesa a través de tu modelo y responde <strong>desde el número mismo</strong>. Sin puertos de entrada, sin cuenta de bot.</p>\n<!-- -->\n<p><strong>Requisitos previos:</strong> Partes 1–4 completadas (OpenClaw en <code>~/openclaw</code>), una cuenta de WhatsApp en tu\nteléfono, ~1 GB de espacio libre en disco. Todos los comandos se ejecutan en <code>~/openclaw</code>.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--agregar-el-canal-de-whatsapp\">Paso 1 — Agregar el canal de WhatsApp<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-whatsapp-channel-openclaw/#paso-1--agregar-el-canal-de-whatsapp\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Agregar el canal de WhatsApp\" title=\"Enlace directo al Paso 1 — Agregar el canal de WhatsApp\" translate=\"no\">​</a></h2>\n<p>OpenClaw trata las aplicaciones de mensajería como <em>canales</em>; WhatsApp es uno de primera clase:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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\">Plugin de WhatsApp instalado</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Cuenta de WhatsApp \"default\" añadida.</span><br></div></code></pre></div></div>\n<details class=\"details_lb9f alert alert--info details_b_Ee\" data-collapsed=\"true\"><summary><b>¿Recibiste <code>requires plugin API &gt;=2026.7.1</code>?</b> Estás en una versión anterior de OpenClaw — actualiza primero.</summary><div><div class=\"collapsibleContent_i85q\"><p>El canal de WhatsApp es un plugin restringido a versiones más nuevas:</p><p><span class=\"zoomImage__wrap\"><img alt=\"El error de versión en OpenClaw 2026.6.8\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 1.</strong> La pared de versión — el plugin de WhatsApp necesita OpenClaw ≥2026.7.1.</p><p>Haz una copia de seguridad, descarga, recrea y confirma que tus canales existentes sobrevivieron (<code>:latest</code> no\n<strong>se actualiza</strong> automáticamente — debes descargar deliberadamente):</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\"># espera 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 debería seguir conectado</span><br></div></code></pre></div></div><p><span class=\"zoomImage__wrap\"><img alt=\"La imagen descargada informa 2026.7.1\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 2.</strong> Después de descargar, la imagen informa 2026.7.1 — se cumple el requisito.</p><p>Luego vuelve a ejecutar el <code>channels add</code> anterior.</p></div></div></details>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--confiar-en-el-plugin\">Paso 2 — Confiar en el plugin<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-whatsapp-channel-openclaw/#paso-2--confiar-en-el-plugin\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — Confiar en el plugin\" title=\"Enlace directo al Paso 2 — Confiar en el plugin\" translate=\"no\">​</a></h2>\n<p>Verifica el estado y el canal está <strong>detenido</strong> con un error críptico:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"WhatsApp detenido con el error de confianza openKeyedStore\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 3.</strong> <code>detenido</code> — un plugin no confiable no puede usar <code>openKeyedStore</code>, por lo que el canal no se iniciará.</p>\n<p>Esa es una <strong>característica de seguridad</strong>: OpenClaw aísla los plugins de terceros, y uno no confiable no puede usar <code>openKeyedStore</code> (el almacenamiento que WhatsApp necesita para su sesión). Confía en él —\nincluye cualquier otro plugin no empaquetado que ya esté cargando (aquí <code>codex</code>) para que no lo bloquees\n— y recrea:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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 ahora lista codex y whatsapp\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 4.</strong> Confiado — <code>plugins.allow</code> ahora lista <code>whatsapp</code> (y <code>codex</code>).</p>\n<p>El reinicio es importante — la puerta de enlace lee <code>plugins.allow</code> <em>antes</em> de inicializar el\nplugin. Ahora está en funcionamiento:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"WhatsApp funcionando y conectado, Telegram sigue activo\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 5.</strong> En vivo — WhatsApp <code>funcionando, conectado, saludable</code>, y Telegram sigue activo.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--vincula-tu-teléfono-por-qr\">Paso 3 — Vincula tu teléfono por QR<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-whatsapp-channel-openclaw/#paso-3--vincula-tu-tel%C3%A9fono-por-qr\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — Vincula tu teléfono por QR\" title=\"Enlace directo al Paso 3 — Vincula tu teléfono por 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>Escanea el QR del terminal desde <strong>WhatsApp → Configuración → Dispositivos Vinculados → Vincular un Dispositivo</strong>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El QR de emparejamiento, vinculado después del reinicio del código 515\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 6.</strong> Escanea para vincular; <code>✅ Vinculado después del reinicio</code> maneja la reconexión del <code>código 515</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>El reinicio del <code>código 515</code> es normal</div><div class=\"admonitionContent_BuS1\"><p>WhatsApp obliga a una reconexión justo después del emparejamiento; OpenClaw lo maneja automáticamente. Este\nes exactamente el lugar donde las configuraciones manuales se quedan atrapadas en un bucle — aquí simplemente funciona.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--pruébalo\">Paso 4 — Pruébalo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-whatsapp-channel-openclaw/#paso-4--pru%C3%A9balo\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — Pruébalo\" title=\"Enlace directo al Paso 4 — Pruébalo\" translate=\"no\">​</a></h2>\n<p>Desde <strong>otro teléfono</strong>, envía un mensaje al número:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El agente de OpenClaw respondiendo en WhatsApp\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 7.</strong> Una pregunta enviada, la respuesta del agente — enviada desde el número, como la cuenta.</p>\n<p>Responde — pero nota que responde <strong>desde tu número, como tú.</strong> Eso es el modo compañero:\nel agente <em>es</em> la cuenta. Genial para un asistente personal, y la razón de la advertencia\na continuación.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Tu agente de OpenClaw responde en WhatsApp — de forma nativa, sin puente, sin cuenta de bot.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"antes-de-un-número-real\">Antes de un número real<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-whatsapp-channel-openclaw/#antes-de-un-n%C3%BAmero-real\" class=\"hash-link\" aria-label=\"Enlace directo al Antes de un número real\" title=\"Enlace directo al Antes de un número real\" 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>Con un número personal, el agente responde a todos, como tú</div><div class=\"admonitionContent_BuS1\"><p>Procesa <strong>cada DM y grupo</strong> que recibe la cuenta (<code>Escuchando ... DM + todos los grupos</code>). Para cualquier cosa real: vincula un <strong>número dedicado</strong> (una SIM prepago barata — los números virtuales son a menudo rechazados), y establece una <strong>lista de permitidos</strong> antes de exponerlo.</p></div></div>\n<p>Estaciona el canal cuando termines de probar (Telegram sigue funcionando; tu enlace permanece guardado):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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=\"solución-de-problemas\">Solución de problemas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-whatsapp-channel-openclaw/#soluci%C3%B3n-de-problemas\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas\" title=\"Enlace directo al Solución de problemas\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong><code>openKeyedStore solo está disponible para plugins confiables</code></strong> — el canal se instala pero permanece\n<code>detenido</code>. Agrega el id a <code>plugins.allow</code> y <strong>recrea</strong> la puerta de enlace (Paso 2). Un cambio de configuración en vivo no es suficiente una vez que ya ha fallado al iniciar.</li>\n<li class=\"\"><strong>Bucle de QR / <code>código 515</code></strong> — reinicio normal después del emparejamiento. Si nunca se estabiliza, elimina\nentradas obsoletas en WhatsApp → Dispositivos Vinculados y escanea un QR nuevo.</li>\n<li class=\"\"><strong>Respondió a un contacto real</strong> — el modo compañero responde a todo; consulta la advertencia\nanterior y usa un número dedicado.</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=\"qué-sigue\">¿Qué sigue?<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-whatsapp-channel-openclaw/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al ¿Qué sigue?\" title=\"Enlace directo al ¿Qué sigue?\" translate=\"no\">​</a></h2>\n<p>Parte 5 completada — tu agente llega a Telegram <em>y</em> WhatsApp desde una caja privada y reforzada.</p>\n<ul>\n<li class=\"\">Dale <strong>memoria que sobreviva a los reinicios</strong> — la serie del <strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/\">Agente Hermes</a></strong>.</li>\n<li class=\"\">Fundamenta en <em>tus</em> documentos — la serie de <strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/\">evaluaciones de IA y\nobservabilidad</a></strong> construye un servicio RAG al que puedes apuntar un canal.</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"desmantelamiento\">Desmantelamiento<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-whatsapp-channel-openclaw/#desmantelamiento\" class=\"hash-link\" aria-label=\"Enlace directo al Desmantelamiento\" title=\"Enlace directo al Desmantelamiento\" 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>Y desvincula el dispositivo en <strong>WhatsApp → Dispositivos Vinculados</strong> si has terminado.</p>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/add-whatsapp-channel-openclaw/",
            "title": "Agregar un canal de WhatsApp a OpenClaw",
            "summary": "Coloca tu agente autoalojado en WhatsApp. Agrega el canal, confía en el plugin, escanea un QR — y entiende por qué un enlace de compañero hace que el agente actúe como tu cuenta. Cada comando y error de una ejecución real.",
            "date_modified": "2026-07-20T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "autoalojamiento",
                "openclaw",
                "whatsapp",
                "chatbot"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__deepeval\" src=\"data:image/png;base64,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\" alt=\"DeepEval\"><span class=\"tutorialHero__plus\">+</span><svg viewBox=\"0 0 24 24\" fill=\"#2496ED\" aria-label=\"Docker\" class=\"tutorialHero__docker\"><path d=\"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z\"></path></svg><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 8 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->8</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->8<!-- --> earned</span></div><div class=\"skillTracker__series\">AI evals &amp; observability</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove a model works</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trustworthy JSON</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real test data at scale</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Observe &amp; score production</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">RAG, end to end</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">6</span><span class=\"skillTracker__skill\" data-state=\"current\">Catch regressions in CI</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trace &amp; debug agent tool calls</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/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>Nuestro <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/news/gpt-5-6-token-economics/\">análisis de economía de tokens de GPT-5.6</a> terminó con un desafío:\nlas promesas de referencia son una hipótesis, no una razón — ejecuta la evaluación en <em>tu</em> carga de trabajo antes de que\ncreas en un reclamo de \"menos tokens por tarea\". Este tutorial es nosotros siguiendo nuestro propio consejo.</p>\n<p>Tomamos el asistente de documentos RAG de <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/\">parte 5</a>,\nlo envolvemos en una <strong>suite de regresión DeepEval</strong> — en contenedor, juzgada por <code>gemma4</code> en la API de\nInferencia WEC, sin dependencia de OpenAI — y luego usamos esa suite para responder a una pregunta genuinamente abierta:\n<strong>¿deberíamos cambiar el modelo de generación del servicio?</strong> Qwen2.5-3B-Instruct (actual) vs\ngemma4 (candidato), tasa de aprobación y tokens por tarea, medidos cara a cara.</p>\n<p>Spoiler: la evaluación nos salvó de una migración innecesaria. Y en el camino encontramos cuatro fallos reales\nde producción: un espiral mortal por disco lleno, una condición de carrera en el arranque, una trampa de variable de Docker Compose\nque evaluó silenciosamente el <em>modelo incorrecto</em>, y la solución que hace que esa clase de\nerror sea imposible. Cada comando, número y error a continuación proviene de una ejecución real.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-estamos-construyendo\">Lo que estamos construyendo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/#lo-que-estamos-construyendo\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que estamos construyendo\" title=\"Enlace directo al Lo que estamos construyendo\" translate=\"no\">​</a></h2>\n<!-- -->\n<p>Tres decisiones de diseño desde el principio:</p>\n<ol>\n<li class=\"\"><strong>El ejecutor de evaluación es un contenedor</strong>, no un venv. La suite que ejecutas localmente es byte por byte\nel artefacto que ejecuta CI — <code>docker compose run eval</code> en ambos lugares. (Nuestro primer intento fue un\nvenv. Murió por un disco lleno antes de instalar nada; consulta Solución de problemas.)</li>\n<li class=\"\"><strong>El juez es gemma4 en WEC.</strong> DeepEval asume OpenAI por defecto; le damos una clase de modelo personalizada de 30 líneas en su lugar. Las preguntas, respuestas y veredictos nunca salen de nuestra infraestructura.</li>\n<li class=\"\"><strong>Reutilizamos el conjunto dorado de la parte 5 sin cambios.</strong> El <code>tests.csv</code> de promptfoo incrusta cada\nhecho de referencia dentro de una cadena <code>llm-rubric</code> — una expresión regular lo extrae. Tus datos de evaluación\nsobreviven a tu marco de evaluación.</li>\n</ol>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">El <strong>servicio RAG en funcionamiento de <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/\">parte 5</a></strong>\n(<code>rag-api</code> respondiendo en <code>:8000</code>, <code>eval/tests.csv</code> presente).</li>\n<li class=\"\">Una <strong>clave de API de Inferencia WEC</strong> (<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/cloud_portal/platform/inference/api_keys/\">Inferencia → Claves de API</a>).</li>\n<li class=\"\"><strong>~2 GB de disco libre.</strong> En serio — verifica <code>df -h /</code> <em>ahora</em>. El nuestro leía 100% y la historia de por qué\nestá en Solución de problemas.</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-0--prueba-de-humo-del-sistema-bajo-prueba\">Paso 0 — prueba de humo del sistema bajo prueba<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/#paso-0--prueba-de-humo-del-sistema-bajo-prueba\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 0 — prueba de humo del sistema bajo prueba\" title=\"Enlace directo al Paso 0 — prueba de humo del sistema bajo prueba\" translate=\"no\">​</a></h2>\n<p>Nunca evalúes un servicio que no hayas probado manualmente primero:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\": \"¿Cómo creo una instancia de cómputo en 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=\"El servicio RAG respondiendo a una pregunta de documentos con fuentes\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 1.</strong> El sistema bajo prueba, vivo: <code>/ask</code> responde del corpus de documentos de WEC y cita\nsus fuentes. Nunca evalúes un servicio que no hayas probado manualmente primero.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--el-ejecutor-de-evaluación-en-contenedor\">Paso 1 — el ejecutor de evaluación en contenedor<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/#paso-1--el-ejecutor-de-evaluaci%C3%B3n-en-contenedor\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — el ejecutor de evaluación en contenedor\" title=\"Enlace directo al Paso 1 — el ejecutor de evaluación en contenedor\" 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\"># El código de la suite se monta en tiempo de ejecución para que las ediciones no necesiten una reconstrucción</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>Agrega el servicio a <code>docker-compose.yml</code> <strong>dentro del bloque <code>services:</code></strong> (el archivo de la parte 5 termina\ncon una sección <code>volumes:</code> de nivel superior — agregar ciegamente coloca tu servicio en el bloque incorrecto;\n<code>docker compose config --quiet</code> lo detecta):</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 (nuevo servicio, dentro de 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> lo mantiene fuera de un <code>docker compose up</code> normal — solo se ejecuta cuando lo pides.\nConstruyelo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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 construyendo la imagen del ejecutor de evaluación\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 2.</strong> El ejecutor de evaluación se construye en ~42s: <code>python:3.12-slim</code> más DeepEval 3.x. Esta imagen exacta\nes la que CI ejecuta más tarde — sin venv, sin desviaciones.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--un-juez-en-tu-propia-infraestructura\">Paso 2 — un juez en tu propia infraestructura<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/#paso-2--un-juez-en-tu-propia-infraestructura\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — un juez en tu propia infraestructura\" title=\"Enlace directo al Paso 2 — un juez en tu propia infraestructura\" translate=\"no\">​</a></h2>\n<p>Las métricas de LLM-como-juez de DeepEval quieren una clave de OpenAI. No tenemos una y no queremos una.\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\">\"\"\"Juez personalizado de DeepEval respaldado por WiLine Inference (compatible con OpenAI).\"\"\"</span><span class=\"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>Prueba que responde desde dentro de la red de compose antes de construir nada sobre ello:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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('el juez dice:', j.generate('Responde exactamente: juez WEC en línea'))\"</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\">Salida</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\">el juez dice: juez WEC en línea</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Verificación de conectividad del juez desde dentro de la red de compose\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 3.</strong> \"juez WEC en línea\" — gemma4 respondiendo a través del contenedor, en la misma red de compose que el servicio que juzgará. Sin clave de OpenAI en ninguna parte de la pila.</p>\n<p>Una advertencia honesta que llevamos a través del resto de este tutorial: en la comparación de modelos más adelante,\n<strong>gemma4 es tanto juez como concursante</strong>. La mitigación es estructural: el juez solo compara\nrespuesta frente a hecho de referencia, nunca \"qué modelo escribió esto\" — y observamos sus veredictos en los\ncasos conocidos difíciles para favoritismo. (No mostró ninguno: cada caso en el que falló gemma4 es un caso en el que también falla Qwen.)</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes apuntar las métricas juzgadas por LLM de DeepEval a cualquier punto final compatible con OpenAI. Tus veredictos de evaluación — preguntas, respuestas, razonamiento — nunca salen de tu infraestructura.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--expón-lo-que-quieres-medir\">Paso 3 — expón lo que quieres medir<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/#paso-3--exp%C3%B3n-lo-que-quieres-medir\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — expón lo que quieres medir\" title=\"Enlace directo al Paso 3 — expón lo que quieres medir\" translate=\"no\">​</a></h2>\n<p>El <code>/ask</code> de la parte 5 devolvió <code>answer</code> y <code>sources</code> — y desechó el uso de tokens que venía con cada finalización. Para la economía de tokens lo necesitamos, y para el A/B necesitamos que el modelo sea intercambiable. Tres variables de entorno y dos campos de respuesta en <code>app/main.py</code> (montado en volumen; uvicorn\nrecarga en caliente, sin reconstrucción):</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 (extracto)</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\"># El modelo de generación es configurable para que la suite de evaluación pueda A/B modelos.</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\"># Los valores predeterminados preservan la configuración del tutorial-5.</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> ahora también devuelve <code>resp.usage</code>, y <code>/ask</code> informa ambos:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Salida (abreviada)</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\">\"Para ver tus estados de cuenta ...\"</span><span class=\"token punctuation\" style=\"color:#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 respuesta ahora informando modelo y uso de tokens\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 4.</strong> Los nuevos campos <code>model</code> y <code>usage</code>: 1,238 tokens de prompt + 122 tokens de finalización para una\npregunta de facturación. Este es el número del debate sobre economía de tokens.</p>\n<p>Ese bloque <code>usage</code> es el número del marketing de GPT-5.6 — medido en tu carga de trabajo,\npor solicitud. Pasa la variable a través de compose (<code>GEN_MODEL: ${GEN_MODEL:-Qwen2.5-3B-Instruct}</code>\nbajo <code>rag-api</code>'s <code>environment:</code>) y recuerda esta línea — vuelve a morder en\nSolución de problemas.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--la-suite-de-regresión\">Paso 4 — la suite de regresión<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/#paso-4--la-suite-de-regresi%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — la suite de regresión\" title=\"Enlace directo al Paso 4 — la suite de regresión\" translate=\"no\">​</a></h2>\n<p><code>eval/test_rag_regression.py</code>. Lee el <code>tests.csv</code> de la parte 5, extrae el hecho de referencia de\ncada rúbrica de promptfoo, pregunta al servicio en vivo, registra el uso de tokens y afirma una métrica de corrección\nde GEval juzgada por <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\">\"\"\"Suite de regresión DeepEval para el servicio RAG de documentos WEC.</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\">Reutiliza el conjunto dorado de promptfoo (tests.csv) del tutorial anterior:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">el hecho de referencia se analiza de cada cadena llm-rubric.</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 = todos</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'Hecho de referencia de los documentos oficiales: \"(.*?)\"'</span><span class=\"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\">\"La salida real debe abordar correctamente la pregunta de entrada y no debe \"</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\">\"contradecir el hecho de referencia dado como salida esperada. Diferente redacción \"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:#e3116c\">\"está bien. Fallar solo si la respuesta es incorrecta, contradice el hecho de referencia, \"</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\">\"o no responde a la pregunta.\"</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\"la suite esperaba </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\"> pero el servicio está ejecutando </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>(Esa afirmación <code>EXPECT_MODEL</code> no estaba en la primera versión. Existe debido a un error que encontrarás en un momento.)</p>\n<p>Primera ejecución, limitada a 5 casos para fallar rápido:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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 de 5 pasaron en 2:37</strong> — y tanto los pases como la falla valen la pena leer. La\nfalla (\"¿Cuál es el punto de partida para la facturación?\", puntuación 0.3) no es un error del servicio: la\nrespuesta era factualmente correcta, pero la etiqueta dorada es un <em>título</em> de sección (\"Cómo Funciona la Facturación\")\ny el juez quería que se nombrara. Recuerda este caso — parpadea entre pasar y fallar durante todo el tutorial, y ese parpadeo se convierte en un hallazgo. Mientras tanto, en \"número total de eventos\"\nel juez puntuó un 0.6, un pase límite con el razonamiento exactamente correcto (el servicio\ndescribió la característica en lugar de dar pasos). Veredictos matizados de un juez autoalojado —\nla cosa que la gente asume que necesitas modelos de clase GPT-4 para.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Primera ejecución de DeepEval: 4 de 5 pasaron, con razonamiento del juez\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 5.</strong> Primera ejecución: 4 de 5 pasaron en 2:37, cada veredicto con razonamiento escrito del\njuez gemma4 — incluyendo la única falla, un veredicto estricto sobre una etiqueta de título de sección\nen lugar de un error del servicio.</p>\n<p>Nota la línea de resumen de DeepEval: <code>costo de token: Ninguno</code>. No puede calcular el precio de un juez autoalojado — que es\nexactamente por qué la suite escribe su propio <code>usage_log.jsonl</code>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Registro de tokens por pregunta escrito por la suite\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 6.</strong> La contabilidad propia de la suite: una línea JSON por pregunta — modelo, prompt, finalización,\ntotal — los datos en bruto de cada comparación en este tutorial se construyen a partir de esto.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Tienes una suite de regresión que califica un servicio RAG en vivo — reutilizando el conjunto dorado del último tutorial\nsin cambios, juzgada por un modelo autoalojado, con contabilidad de tokens por pregunta que el marco\nmismo no puede proporcionarte.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5--el-experimento-qwen25-3b-vs-gemma4\">Paso 5 — el experimento: Qwen2.5-3B vs gemma4<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/#paso-5--el-experimento-qwen25-3b-vs-gemma4\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 5 — el experimento: Qwen2.5-3B vs gemma4\" title=\"Enlace directo al Paso 5 — el experimento: Qwen2.5-3B vs gemma4\" translate=\"no\">​</a></h2>\n<p>La pregunta que la suite existe para responder: nuestro servicio genera con Qwen2.5-3B-Instruct —\n¿debería hacerlo? gemma4 es más nuevo, bien considerado, ya está en el catálogo de WEC. Se siente como una mejora.\n<em>Se siente.</em> Vamos a medir.</p>\n<p>Línea base, todos los 16 casos:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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%) en 8:50.</strong> Las dos fallas son artefactos de etiqueta, no errores de RAG: los hechos dorados\nde la parte 5 son <em>títulos</em> de sección (\"Cómo Funciona la Facturación\"), y el juez penalizó respuestas que eran\nfactualmente correctas pero no repetían el título; y <code>mp3</code> frente a la etiqueta <code>audio/mpeg</code> — el mismo hecho en\ndos niveles de abstracción, dictaminado como una contradicción. Lee tus fallas antes de confiar en tu tasa de aprobación. Dejamos ambas: son una deuda de etiqueta honesta, y se convierten en nuestro canario de favoritismo del juez en el A/B.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Línea base de Qwen: 14 de 16 pasaron\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 7.</strong> La línea base: 87.5% en 8:50. Ambas fallas son artefactos de etiqueta, no errores de RAG —\nlee tus fallas antes de confiar en tu tasa de aprobación.</p>\n<p>Desafiante — misma suite, una variable de entorno (prefíjela en <strong>cada</strong> comando de compose; Solución de problemas\nexplica por qué en sangre):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash 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\">\"esperando...\"</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; el modelo es:', 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>Y verifica el registro, porque las etiquetas de ejecución mienten:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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=\"Cambiando el servicio a gemma4 y verificando desde la respuesta\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 8.</strong> Cambiando el desafiante: <code>GEN_MODEL=gemma4</code>, espera por <code>/health</code>, luego confirma\n<code>&gt;&gt;&gt; el modelo es: gemma4</code> desde la propia respuesta — antes de gastar un solo minuto de evaluación.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Resumen de ejecución de gemma4: 13 de 16 pasaron\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 9.</strong> La ejecución de gemma4: 81.25% en ~13 minutos — y sus tres fallas son los mismos\ncasos de artefactos de etiqueta en los que Qwen parpadea. Sin favoritismo del juez gemma4 hacia el concursante gemma4.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-tabla-de-veredictos\">La tabla de veredictos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/#la-tabla-de-veredictos\" class=\"hash-link\" aria-label=\"Enlace directo al La tabla de veredictos\" title=\"Enlace directo al La tabla de veredictos\" translate=\"no\">​</a></h3>\n<p>Un script sobre los registros de uso:</p>\n<table><thead><tr><th>ejecución</th><th>tasa de aprobación</th><th>tokens promedio de prompt</th><th>tokens promedio de finalización</th><th>tokens totales</th></tr></thead><tbody><tr><td>Qwen2.5-3B (ejecución 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 (ejecución 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=\"Tabla de comparación de tokens: Qwen vs gemma4\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 10.</strong> El veredicto en cinco líneas: misma tasa de aprobación, 5.5× los tokens de finalización. La evaluación\njusto previno una migración innecesaria.</p>\n<p>Dos hallazgos, uno esperado y uno no:</p>\n<p><strong>El veredicto: no cambies.</strong> Misma calidad efectiva, mismos casos de falla — pero gemma4 usa\n<strong>5.5× los tokens de finalización</strong> (415 frente a 75 por respuesta) y se ejecuta ~45% más lento por pregunta. En una\ncarga de trabajo RAG donde el contexto recuperado de ~1,100 tokens domina el prompt, la columna de finalización es toda la diferencia de costo. \"Modelo más nuevo\" perdió ante \"mídelo\" en una tarde. Esta es precisamente la evaluación que el post de noticias de GPT-5.6 dijo que ejecutarías — y es la misma evaluación que ejecutarías\ncontra GPT-5.6 mismo: cambia <code>GEN_MODEL</code>, <code>GEN_BASE_URL</code>, y <code>GEN_API_KEY</code>, nada más.</p>\n<p><strong>El juez tiene un umbral de ruido.</strong> Mira las dos filas de Qwen: <strong>cuentas de tokens idénticas</strong> —\n19,225 en ambas ejecuciones, porque la generación con temperatura 0 es determinista — sin embargo 87.5% frente a 81.25%.\nLas respuestas no cambiaron; solo los veredictos del juez lo hicieron (un límite de 0.6 se convirtió en un 0.3).\nEso aísla limpiamente la no determinación del juez del comportamiento del modelo, y establece tu umbral de CI:\nuna puerta en \"≥ 87.5%\" fallaría. El nuestro tolera un veredicto cambiado y falla en dos o más\nregresiones reales.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Ejecución 2 de Qwen: respuestas idénticas, veredictos diferentes\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 11.</strong> Ejecución 2 de Qwen: las mismas 16 respuestas deterministas, 81.25% esta vez — el juez, no\nel modelo, es donde vive la variación.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Puedes resolver un debate sobre el cambio de modelo con datos medidos — tasa de aprobación <em>y</em> tokens por tarea — y\nconoces el umbral de ruido de tu juez, así que puedes establecer un umbral de CI que no falle.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-6--la-puerta-de-ci\">Paso 6 — la puerta de CI<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/#paso-6--la-puerta-de-ci\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 6 — la puerta de CI\" title=\"Enlace directo al Paso 6 — la puerta de CI\" translate=\"no\">​</a></h2>\n<p>La recompensa de contenerizar todo: el trabajo de CI es el comando que has estado ejecutando todo\nel tiempo. <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\"># corredor con acceso a 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\"> Ejecutar suite de regresión</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\"> Desmontar</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>Cualquier PR que toque la aplicación o la evaluación debe sobrevivir a 16 preguntas juzgadas contra el servicio en vivo — con la identidad del modelo afirmada — antes de que se fusione.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Tu evaluación es una puerta de CI. El contenedor que ejecutaste localmente es byte por byte lo que CI ejecuta — sin\n\"funciona en mi máquina\" brecha entre desarrollo y la tubería.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-de-problemas--las-cuatro-fallas-reales\">Solución de problemas — las cuatro fallas reales<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/#soluci%C3%B3n-de-problemas--las-cuatro-fallas-reales\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas — las cuatro fallas reales\" title=\"Enlace directo al Solución de problemas — las cuatro fallas reales\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"errno-28-no-hay-espacio-en-el-dispositivo--y-volvió\"><code>[Errno 28] No hay espacio en el dispositivo</code> — y volvió<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/#errno-28-no-hay-espacio-en-el-dispositivo--y-volvi%C3%B3\" class=\"hash-link\" aria-label=\"Enlace directo al errno-28-no-hay-espacio-en-el-dispositivo--y-volvió\" title=\"Enlace directo al errno-28-no-hay-espacio-en-el-dispositivo--y-volvió\" translate=\"no\">​</a></h3>\n<p>Nuestra primera <code>pip install deepeval</code> falló: disco 100% lleno, 58G de 58G. Liberamos 2 GB de registros de diario\ny caché de construcción… y estaba lleno de nuevo <em>minutos después</em>. El culpable:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Salida (el culpable)</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> estaba en un bucle de error, rociando trazas de pila en un registro json de Docker con\n<strong>sin rotación configurada</strong> — un espiral mortal, porque un disco lleno causa más errores que causa más\nregistros. Había estado \"no saludable\" durante 8 días. La solución, en orden (el orden importa — truncar mientras\nel contenedor se ejecuta simplemente vuelve a llenar):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Luego hazlo permanente — rotación de registros en la sobrecarga de compose, aplicada a cada servicio:</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 (pila de Langfuse)</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\"># ... cada otro servicio</span><br></div></code></pre></div></div>\n<p><code>docker compose up -d</code> para recrear. ClickHouse se volvió saludable por primera vez en 8 días.\nEl controlador json-file de Docker <strong>no</strong> rota por defecto; en una caja que ejecuta infraestructura LLM\nque registra entusiastamente, los registros de contenedor sin límites son una bomba de tiempo.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"13-pruebas-fallan-con-conexión-rechazada-luego-3-pasan\">13 pruebas fallan con <code>Conexión rechazada</code>, luego 3 pasan<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/#13-pruebas-fallan-con-conexi%C3%B3n-rechazada-luego-3-pasan\" class=\"hash-link\" aria-label=\"Enlace directo al 13-pruebas-fallan-con-conexión-rechazada-luego-3-pasan\" title=\"Enlace directo al 13-pruebas-fallan-con-conexión-rechazada-luego-3-pasan\" translate=\"no\">​</a></h3>\n<p>Cambiar <code>GEN_MODEL</code> recrea <code>rag-api</code>, y la aplicación tarda ~3 minutos en arrancar (Chroma +\ncarga del modelo de incrustación). Nuestra suite comenzó a golpear <code>/ask</code> inmediatamente: 13 fallas\nde conexión rechazadas seguidas, luego la API cobró vida a mitad de ejecución y las últimas 3 pasaron. Flaky de la peor manera — la\nfalla depende del tiempo de reloj de pared.</p>\n<p>Soluciona esto en la suite, no en la disciplina humana — <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\">\"\"\"Rechaza iniciar la suite hasta que la API RAG esté saludable.\"\"\"</span><span class=\"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 no saludable después de </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 — abortando evaluación\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-trampa-de-variable-de-compose-evaluamos-el-modelo-incorrecto-dos-veces\">La trampa de variable de compose: evaluamos el modelo incorrecto. Dos veces.<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/#la-trampa-de-variable-de-compose-evaluamos-el-modelo-incorrecto-dos-veces\" class=\"hash-link\" aria-label=\"Enlace directo al La trampa de variable de compose: evaluamos el modelo incorrecto. Dos veces.\" title=\"Enlace directo al La trampa de variable de compose: evaluamos el modelo incorrecto. Dos veces.\" translate=\"no\">​</a></h3>\n<p>La más desagradable, porque nada falló. <code>${GEN_MODEL:-Qwen…}</code> en compose se interpola\ndesde <em>tu shell</em> en <em>cada</em> invocación. Comenzamos el servicio con\n<code>GEN_MODEL=gemma4 docker compose up -d</code> — correcto. Luego ejecutamos la evaluación <strong>sin</strong> el prefijo.\nCompose resolvió la variable de nuevo al valor predeterminado de Qwen, vio el contenedor en ejecución como un desvío de configuración\ny — a través de <code>depends_on</code> — <strong>silenciosamente recreó <code>rag-api</code> con Qwen</strong>. La ejecución se completó,\nel resumen decía \"gemma4\" en cada veredicto (el <em>juez</em> era gemma4), y cada respuesta había sido\nescrita por Qwen.</p>\n<p>Solo lo atrapamos porque la suite registra el modelo <em>desde la respuesta</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     ← la ejecución \"gemma4\"</span><br></div></code></pre></div></div>\n<p>Nunca confíes en la etiqueta de una ejecución; verifica desde los datos que el sistema bajo prueba informa sobre sí mismo.\nLa solución permanente es la afirmación <code>EXPECT_MODEL</code> en la suite (Paso 4). Prueba que funciona — la\nprueba de fallo deliberado:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Salida</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: la suite esperaba gemma4 pero el servicio está ejecutando Qwen2.5-3B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">1 falló en 2.44s</span><br></div></code></pre></div></div>\n<p>Menos de tres segundos, antes de gastar una sola llamada de juez — frente a los 10+ minutos que cada ejecución mal etiquetada\nnos costó.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Guardia EXPECT_MODEL fallando ruidosamente en el modelo incorrecto\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 12.</strong> La guardia en acción: modelo incorrecto → fallo ruidoso en menos de tres segundos, antes de que se gaste una\nsola llamada de juez. Compara con los 10+ minutos que cada ejecución mal etiquetada nos costó.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"sleep-5-no-es-una-verificación-de-disponibilidad\"><code>sleep 5</code> no es una verificación de disponibilidad<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/#sleep-5-no-es-una-verificaci%C3%B3n-de-disponibilidad\" class=\"hash-link\" aria-label=\"Enlace directo al sleep-5-no-es-una-verificación-de-disponibilidad\" title=\"Enlace directo al sleep-5-no-es-una-verificación-de-disponibilidad\" translate=\"no\">​</a></h3>\n<p>Pequeño bono: después de recrear el contenedor, canalizamos la salida de <code>/ask</code> a un analizador JSON 5\nsegundos después y obtuvimos <code>json.decoder.JSONDecodeError: Esperando valor: línea 1 columna 1</code> — el\nservidor no estaba arriba, curl devolvió vacío, el analizador se ahogó en nada. Sondea <code>/health</code> en un bucle;\nnunca duermas un número adivinado.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-construiste\">Lo que construiste<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/#lo-que-construiste\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que construiste\" title=\"Enlace directo al Lo que construiste\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">Una <strong>suite de regresión DeepEval en contenedor</strong> — misma imagen localmente y en CI</li>\n<li class=\"\">Un <strong>juez personalizado en WEC Inference</strong> (<code>gemma4</code> + GEval): veredictos con razonamiento, sin clave de OpenAI</li>\n<li class=\"\"><strong>Contabilidad de tokens por pregunta</strong> que el marco mismo no podía darte</li>\n<li class=\"\">Una <strong>decisión de modelo medida</strong>: gemma4 ≈ Qwen en calidad, 5.5× los tokens de finalización — mantén Qwen</li>\n<li class=\"\">Una <strong>medición del umbral de ruido del juez</strong> (respuestas deterministas, un cambio de tasa de aprobación de 6.25 puntos) que establece tu umbral de CI de manera honesta</li>\n<li class=\"\">Una <strong>guardia <code>EXPECT_MODEL</code></strong> que convierte la evaluación silenciosa del modelo incorrecto en un fallo ruidoso en menos de tres segundos</li>\n<li class=\"\">Una <strong>puerta de GitHub Actions</strong> que ejecuta el contenedor idéntico en cada 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=\"qué-sigue\">Qué sigue<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al Qué sigue\" title=\"Enlace directo al Qué sigue\" translate=\"no\">​</a></h2>\n<p>La suite juzga <em>respuestas</em>. Las trazas de Langfuse de la parte 4 saben <em>por qué</em> — qué fragmentos fueron recuperados,\ncuánto costó el rango de generación, dónde se oculta la latencia. Conectar fallas de evaluación de vuelta a sus trazas\n(ID de prueba de DeepEval ↔ ID de traza de Langfuse) convierte \"la prueba falló\" en \"aquí está la recuperación que\nlo causó.\" Esa es la historia de depuración a nivel de componente, y es hacia donde va esta serie a continuación.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lectura-adicional\">Lectura adicional<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/#lectura-adicional\" class=\"hash-link\" aria-label=\"Enlace directo al Lectura adicional\" title=\"Enlace directo al Lectura adicional\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://deepeval.com/docs/models-custom\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Documentación de DeepEval — modelos personalizados</a></li>\n<li class=\"\"><a href=\"https://deepeval.com/docs/metrics-llm-evals\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">DeepEval — métrica GEval</a></li>\n<li class=\"\"><a href=\"https://docs.docker.com/engine/logging/drivers/json-file/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Controladores de registro de Docker — rotación de json-file</a></li>\n<li class=\"\"><a href=\"https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Interpolación de variables de Compose</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/",
            "title": "Prueba de regresión de tu servicio RAG con DeepEval — y resuelve un debate sobre modelos con datos",
            "summary": "Envuelve el asistente RAG de la parte 5 en una suite de DeepEval en contenedor juzgada por gemma4 en la API de Inferencia WEC — sin clave de OpenAI en ninguna parte. Luego úsalo para responder a una pregunta real: ¿deberíamos cambiar nuestro modelo de generación? La misma calidad, 5.5× los tokens: la evaluación dice que no. Cada comando, número y error es 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",
                "inferencia"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/",
            "content_html": "<div class=\"tutorialHero\"><svg viewBox=\"0 0 24 24\" fill=\"#2496ED\" aria-label=\"Docker\" class=\"tutorialHero__docker\"><path d=\"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z\"></path></svg><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/promptfoo-avatar-31b32c657d72dac0a7edbd77c24ec717.jpg\" alt=\"Promptfoo\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2255\" height=\"527\" fill=\"none\" viewBox=\"0 0 2255 527\" class=\"tutorialHero__langfuse\"><path fill=\"#1B1917\" d=\"M652.669 116.433c0-10.261-7.683-17.956-17.926-17.956H607V60h87.923v316.366h-42.254zM804.056 379.786c-39.693 0-72.131-27.362-72.131-68.831 0-41.042 28.596-69.686 84.935-69.686h39.693c7.256 0 12.805-5.558 12.805-12.826v-8.55c0-25.651-20.487-38.905-40.547-38.905-18.353 0-33.291 8.123-41.828 26.934h-45.668c11.95-44.462 46.095-65.41 88.349-65.41 40.547 0 82.374 22.231 82.374 77.808v156.046h-42.68v-14.109c0-5.13-5.549-7.695-9.817-4.702-16.646 11.97-32.438 22.231-55.485 22.231m5.975-38.477c18.78 0 34.572-9.406 52.498-27.362 4.695-4.702 6.829-10.688 6.829-17.1v-6.413c0-7.268-5.549-12.826-12.805-12.826H815.58c-27.743 0-40.974 12.398-40.974 31.637 0 17.956 12.377 32.064 35.425 32.064M961.855 376.366V147.642h42.255v19.666c0 5.13 6.4 6.84 10.24 2.992 13.23-12.825 31.59-27.788 60.61-27.788 36.71 0 70.85 23.086 70.85 75.671v158.183h-42.25V227.161c0-29.499-17.93-45.745-39.7-45.745-20.91 0-35 11.971-49.93 29.499-7.26 8.978-9.82 19.238-9.82 30.354v135.097zM1287.48 467c-52.5 0-85.79-24.796-95.61-63.273h46.1c7.25 15.818 20.06 25.651 45.67 25.651 34.14 0 55.48-20.093 55.48-65.838v-7.268c0-5.13-4.27-7.695-9.39-3.42-14.08 12.398-32.01 20.093-49.08 20.093-58.05 0-96.46-44.889-96.46-115.003 0-70.113 44.39-115.43 98.17-115.43 15.79 0 30.3 4.275 44.38 14.963 5.98 4.275 12.38.855 12.38-5.986v-3.847h42.26V363.54c0 72.678-43.97 103.46-93.9 103.46m-2.56-132.959c19.2 0 33.29-8.55 44.81-20.949 7.26-8.122 9.39-13.68 9.39-26.506v-61.563c0-12.826-2.13-20.948-10.67-29.071-9.39-8.978-22.62-14.964-39.69-14.964-35 0-61.89 29.072-61.89 76.954 0 47.883 24.76 76.099 58.05 76.099M1455.92 199.372c0-7.268-5.97-13.253-13.23-13.253h-32.44v-38.477h32.44c7.26 0 13.23-5.985 13.23-13.253v-5.986c0-45.744 23.48-68.403 69.15-68.403h29.02v38.477h-29.45c-17.5 0-26.46 9.833-26.46 29.926v5.986c0 7.268 5.97 13.253 13.23 13.253h42.68v38.477h-42.68c-7.26 0-13.23 5.985-13.23 13.253v176.994h-42.26zM1652.02 381.496c-35.85 0-69.14-23.086-69.14-75.671V147.642h42.25v150.06c0 29.499 17.07 44.889 37.13 44.889 21.77 0 35.85-11.97 50.79-29.499 7.26-8.977 9.82-19.238 9.82-30.354V147.642h42.25v228.724h-42.25V356.7c0-5.131-6.4-6.841-10.24-2.993-13.24 12.826-31.59 27.789-60.61 27.789M1893.57 381.496c-38.84 0-79.39-19.239-90.06-65.838h43.54c6.4 17.528 23.9 29.498 44.81 29.498 23.05 0 37.13-13.68 37.13-30.353 0-16.246-11.09-25.224-28.59-30.354l-36.28-10.261c-31.58-8.978-55.06-29.499-55.06-64.556 0-38.049 35.43-67.12 75.55-67.12 32.01 0 70.85 14.535 81.09 65.41h-40.55c-5.55-17.528-20.06-29.071-40.54-29.071-20.06 0-34.58 12.398-34.58 28.216 0 13.253 8.11 23.086 27.32 28.644l34.15 9.833c32.43 9.406 58.47 29.072 58.47 66.693 0 39.332-34.15 69.259-76.4 69.259M2098.54 381.496c-61.46 0-102.01-51.73-102.01-119.706s43.11-119.278 101.58-119.278c63.6 0 96.89 51.302 96.89 109.872v23.087h-144.26c-5.98 0-8.54 3.847-7.26 13.68 4.7 32.064 30.31 54.295 55.49 54.295 18.78 0 35-9.405 45.67-27.788h44.81c-16.22 40.187-49.94 65.838-90.91 65.838m43.11-141.51c6.83 0 9.39-3.42 7.68-14.108-4.69-26.506-24.33-45.317-51.22-45.317-25.6 0-47.37 18.811-54.2 45.745-2.56 9.833.85 13.68 6.83 13.68z\"></path><path fill=\"#FF5D5F\" d=\"m286.292 286.105 34.597 27.791s26.473-19.661 45.941-22.545c20.418-3.025 42.202 8.359 62.388 21.93 30.489 20.498 56.149 46.508 56.149 46.508l30.06-29.493s-82.879-89.795-148.597-81.672c-43.105 5.328-80.538 37.481-80.538 37.481\"></path><path fill=\"#4E9CFF\" d=\"M88.358 114.862 60 146.056s79.009 73.732 141.224 73.732c28.358 0 67.684-22.216 101.523-51.079 19.283-16.448 40.835-35.13 62.388-35.13 14.487 0 33.594 7.673 51.612 27.824 0 0 11.63-6.974 18.716-11.985 6.228-4.404 15.479-11.91 15.479-11.91-25.918-27.663-63.407-47.883-85.807-45.9-36.299.005-62.388 22.601-94.717 48.735s-45.94 36.907-69.194 36.907c-39.134 0-112.866-62.388-112.866-62.388M88.358 352.463 60 321.269s79.009-73.732 141.224-73.732c28.358 0 67.684 22.216 101.523 51.079 19.283 16.448 40.835 35.13 62.388 35.13 14.556 0 33.518-7.989 51.612-28.358 0 0 10.877 6.705 17.582 11.344 6.894 4.769 17.015 12.655 17.015 12.655-25.931 27.883-63.693 48.323-86.209 46.33-36.299-.005-57.851-19.24-90.179-45.374-32.329-26.133-50.478-40.268-73.732-40.268-39.134 0-112.866 62.388-112.866 62.388M458.142 185.149c-7.378 5.1-19.283 12.478-19.283 12.478s6.806 14.746 6.806 34.597-6.239 36.866-6.239 36.866 10.688 6.675 17.582 11.343c7.162 4.849 18.149 13.045 18.149 13.045s13.045-27.224 13.045-61.254-13.045-59.552-13.045-59.552-10.236 7.792-17.015 12.477\"></path><path fill=\"#FF5D5F\" d=\"m287.995 180.612 32.895-27.224s26.473 19.046 45.941 21.93c20.417 3.026 42.202-8.359 62.388-21.93 30.489-20.498 56.149-46.507 56.149-46.507l30.06 29.492s-82.879 89.795-148.597 81.672c-43.105-5.328-78.836-37.433-78.836-37.433M208.601 91c42.538 0 78.264 36.299 78.264 36.299s-9.941 7.832-16.448 13.045c-6.777 5.429-17.582 14.179-17.582 14.179s-18.711-19.851-44.234-19.851c-10.465 0-24.066 6.286-38.567 18.716-11.188 9.591-22.829 21.514-30.627 36.299-6.743 12.784-10.42 27.85-10.776 43.672-.446 19.873 6.597 40.704 18.149 57.283 7.743 11.112 16.983 19.474 26.657 26.657 12.555 9.322 25.648 15.881 35.164 15.881 10.166 0 19.306-3.533 26.09-6.806 10.776-6.239 19.278-13.612 19.278-13.612l33.463 27.791s-13.612 13.612-32.323 23.821c-12.091 5.963-27.632 11.91-46.508 11.91-18.862 0-40.767-10.022-61.254-25.522-13.244-10.021-26.225-21.895-36.298-36.299-16.51-23.607-25.017-52.328-24.96-81.104.057-29.136 9.451-57.993 26.094-81.672C138.273 117.657 176.86 91 208.601 91\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 8 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->8</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->8<!-- --> earned</span></div><div class=\"skillTracker__series\">AI evals &amp; observability</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove a model works</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trustworthy JSON</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real test data at scale</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Observe &amp; score production</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">5</span><span class=\"skillTracker__skill\" data-state=\"current\">RAG, end to end</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">Catch regressions in CI</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trace &amp; debug agent tool calls</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/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>Todo en esta serie apuntaba a esto. Sabes demostrar que un modelo funciona\n(<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/\">parte 1</a>), hacer su salida fiable para\nmáquinas (<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/\">parte 2</a>), generar datos de prueba\nreales (<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/\">parte 3</a>) y observar producción\n(<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/\">parte 4</a>). Ahora gastamos las cuatro habilidades a\nla vez en el patrón que está detrás de casi todo producto LLM serio: <strong>RAG</strong> — generación aumentada\npor recuperación.</p>\n<p>Vamos a construir un <strong>asistente de documentación</strong>: un servicio HTTP en contenedor que responde\npreguntas de clientes desde la propia documentación de WEC. No un notebook — un servicio,\nDocker primero, con la forma que realmente desplegarías. Y como esta serie no hace demos del camino\nfelíz: por el camino nuestro RAG <strong>alucina un precio de GPU</strong>, encontramos la causa en <strong>nuestro\npropio scraper</strong>, la arreglamos y fijamos el arreglo con un test de regresión. Cada comando, número\ny error de abajo viene de una ejecución real.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-vamos-a-construir\">Lo que vamos a construir<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#lo-que-vamos-a-construir\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que vamos a construir\" title=\"Enlace directo al Lo que vamos a construir\" translate=\"no\">​</a></h2>\n<!-- -->\n<p>Una decisión de arquitectura por delante: repartimos el trabajo. <strong>Los embeddings corren\nlocalmente</strong> (un modelo ONNX pequeño — rápido, gratis, sin GPU) y <strong>la generación corre en WEC</strong>.\nEste es un reparto de producción completamente estándar: la recuperación es barata y sensible a la\nlatencia, así que mantenerla junto a tu almacén de vectores ahorra una ida y vuelta de red por\nconsulta — mientras la generación es donde el modelo grande se gana el sueldo.</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>¿Prefieres embeddings gestionados? WEC ya los tiene</div><div class=\"admonitionContent_BuS1\"><p>El catálogo de WEC ahora incluye <strong><code>bge-m3</code></strong> (1024 dimensiones, multilingüe, entrada por lotes) en\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>Pasar este tutorial a esa vía es un cambio de dos funciones — reemplaza las llamadas a <code>fastembed</code>\nen <code>ingest.py</code> y <code>main.py</code> por el mismo cliente de OpenAI apuntando a <code>/v1/embeddings</code>\n(<code>wec.embeddings.create(model=\"bge-m3\", input=chunks)</code>), y vuelve a ingerir (la dimensión del vector\ncambia de 384 a 1024, así que borra y reconstruye la colección). Mantenemos la vía local abajo\nporque es gratis, funciona sin conexión, y cada número de esta guía se midió con ella — pero las dos\nson legítimas en producción; elige según tu latencia y tus preferencias de operación.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong>Docker + Compose</strong> en una instancia WEC (sirve el mismo entorno que la máquina de Langfuse de la\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/\">parte 4</a>).</li>\n<li class=\"\">Una <strong>clave de la API de Inferencia de WEC</strong> (<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/cloud_portal/platform/inference/api_keys/\">Inference → API Keys</a>).</li>\n<li class=\"\">Tu instancia de <strong>Langfuse</strong> de la parte 4 y sus claves (opcional pero recomendado — se usa en el\ntrazado del Paso 3).</li>\n<li class=\"\"><strong>~1 GB de disco libre</strong> para la imagen. Compruébalo primero: <code>df -h /</code>. Nuestra máquina estaba al\n99% y sobrevivió, pero el disco justo es el asesino silencioso número uno de las builds de Docker.</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--montar-el-servicio-docker-primero\">Paso 1 — Montar el servicio, Docker primero<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#paso-1--montar-el-servicio-docker-primero\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Montar el servicio, Docker primero\" title=\"Enlace directo al Paso 1 — Montar el servicio, Docker primero\" translate=\"no\">​</a></h2>\n<p>Las funciones RAG reales se despliegan como servicios, así que empezamos como uno: una app de\nFastAPI en un contenedor, con el código montado para que <code>uvicorn --reload</code> recoja cada edición —\nsin rebuild por cambio.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Los secretos viven en un archivo <code>.env</code> junto al compose (compose lo carga automáticamente — las\nclaves nunca entran en el YAML). Mantenlo fuera del control de versiones desde el principio:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Una app mínima para que el contenedor tenga algo que servir:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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=\"El servicio en contenedor vivo — compose ps + comprobación de salud\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 1.</strong> Un comando cada uno: el contenedor arriba, <code>/health</code> respondiendo, y la comprobación\nde disco (la nuestra marca 99% — esta máquina vive peligrosamente). La build tarda unos 90s y son\nunos 700 MB de imagen.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--ingesta-construir-el-corpus-desde-un-sitio-web-vivo\">Paso 2 — Ingesta: construir el corpus desde un sitio web vivo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#paso-2--ingesta-construir-el-corpus-desde-un-sitio-web-vivo\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — Ingesta: construir el corpus desde un sitio web vivo\" title=\"Enlace directo al Paso 2 — Ingesta: construir el corpus desde un sitio web vivo\" translate=\"no\">​</a></h2>\n<p>En el mundo real, la base de conocimiento de la que quieres que responda tu asistente normalmente no\nes una carpeta ordenada de markdown — es un <strong>sitio web vivo</strong>: documentación de producto, un wiki,\nun centro de ayuda. Así que la habilidad de adquisición de corpus que vale la pena aprender es\nscraping real, no cargar archivos.</p>\n<p>Para este tutorial extraemos <strong>el propio sitio de documentación de WEC</strong>. Está hecho con Docusaurus,\ny Docusaurus (como la mayoría de generadores de sitios) publica un <code>sitemap.xml</code> — lo que significa\nque puedes descubrir cada página programáticamente en vez de adivinar URLs. El mismo enfoque\nfunciona en cualquier sitio que publique un 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>Filtramos a la documentación de producto (<code>/docs/cloud_portal/</code>), traemos cada página, y nos\nquedamos solo con el elemento <code>&lt;article&gt;</code> — Docusaurus envuelve el contenido real en él, así que la\nnavegación, la barra lateral y el pie nunca llegan al índice:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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=\"Ejecución de la ingesta — 37 páginas, 281 trozos, con embeddings e indexados\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 2.</strong> Todo el corpus de documentación de producto indexado en menos de un minuto. El modelo\nde embeddings se guarda en caché en el volumen <code>model-cache</code>, así que se descarga exactamente una\nvez.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Sabes convertir cualquier sitio web de documentación en un índice vectorial consultable —\ndescubrimiento por sitemap, extracción de contenido, troceado, embeddings locales.</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>Sobre <code>get_text(\"\\n\", …)</code></div><div class=\"admonitionContent_BuS1\"><p>Nuestra primera versión usaba <code>get_text(\" \")</code> — un solo espacio como separador. Parecía inofensivo y\n<strong>causó una alucinación auténtica</strong> que verás en el Paso 4. Conserva el salto de línea; volveremos a\nesto. (¿Quieres ver el bug ocurrir en tu propia máquina? Cambia <code>\"\\n\"</code> de vuelta a <code>\" \"</code> en\n<code>scrape()</code>, vuelve a ejecutar la ingesta, y haz la pregunta del Paso 4 — luego devuelve el salto de\nlínea y vuelve a ingerir.)</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>Ruido de telemetría de Chroma</div><div class=\"admonitionContent_BuS1\"><p>Puedes ver <code>Failed to send telemetry event ClientStartEvent: capture() takes 1 positional argument…</code> — un choque conocido de versiones entre chromadb y posthog. Es inofensivo (tus datos\nestán bien), y la variable <code>ANONYMIZED_TELEMETRY: \"False\"</code> del compose lo silencia.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--el-endpoint-ask-recuperar-generar-trazar\">Paso 3 — El endpoint <code>/ask</code>: recuperar, generar, trazar<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#paso-3--el-endpoint-ask-recuperar-generar-trazar\" class=\"hash-link\" aria-label=\"Enlace directo al paso-3--el-endpoint-ask-recuperar-generar-trazar\" title=\"Enlace directo al paso-3--el-endpoint-ask-recuperar-generar-trazar\" translate=\"no\">​</a></h2>\n<p>Ahora el servicio en sí. Tres decisiones de diseño que vale la pena declarar:</p>\n<ul>\n<li class=\"\"><strong>Las citas vienen de los metadatos de recuperación, no del modelo.</strong> El modelo puede alucinar\nURLs; el almacén de vectores no. <code>sources</code> es determinista.</li>\n<li class=\"\"><strong>La llamada a WEC pasa por <code>langfuse.openai</code></strong> (el reemplazo directo de la parte 4), y cada etapa\nrecibe un span con <code>@observe</code> — así que cada petición produce un <strong>árbol\n<code>ask → retrieve → generate</code></strong> en Langfuse.</li>\n<li class=\"\"><strong><code>timeout=60</code></strong> en el cliente. La lección de la parte 4: sin timeout por defecto, un backend\natascado cuelga tu servicio en silencio.</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>Hazle una pregunta real de cliente:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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=\"Primera respuesta real — pasos correctos, fuentes correctas\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 3.</strong> Al primer intento: el flujo exacto de la clave de API sacado de la documentación, con\nla página correcta citada. Extraer → embeddings → recuperar → generar, de punta a punta.</p>\n<p>Abre Langfuse → Tracing y encontrarás la petición como un <strong>árbol de spans</strong> — <code>ask → retrieve → generate → rag-generate</code> — con latencia y tokens por paso. Cuando una respuesta es mala, así\ndistingues <em>mala recuperación</em> de <em>mala generación</em> (la habilidad de la parte 4, ahora en una app\nreal).</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La petición RAG como árbol de spans en Langfuse\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 4.</strong> Cada <code>/ask</code> se convierte en un árbol: la recuperación y la generación tienen cada una\nsu dirección.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Tienes un servicio RAG trazado y en contenedor respondiendo desde documentación extraída en vivo.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--la-alucinación-que-cazamos-de-verdad\">Paso 4 — La alucinación que cazamos (de verdad)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#paso-4--la-alucinaci%C3%B3n-que-cazamos-de-verdad\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — La alucinación que cazamos (de verdad)\" title=\"Enlace directo al Paso 4 — La alucinación que cazamos (de verdad)\" translate=\"no\">​</a></h2>\n<p>Un buen RAG tiene que negarse a lo que la documentación no cubre. Pínchalo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Nuestra primera versión (la del scraper con <code>get_text(\" \")</code>) respondió:</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>La negativa es correcta. Pero vuelve a leer la frase del precio: <em>\"$0.66/GPU-hr <strong>con</strong> una opción\nde prepago anual con 25% de descuento <strong>a $0.10/GB-mo</strong> para almacenamiento en bloque\"</em> — son tres\nhechos distintos soldados en una sola afirmación falsa, pegando una tarifa de almacenamiento a la\nopción de prepago de GPU. Consulta la página real de precios y descubres que simplemente son tres\nelementos adyacentes:</p>\n<ul>\n<li class=\"\">GPU por hora: <strong>$0.66/GPU-hr</strong></li>\n<li class=\"\">Prepago anual: <strong>25% de descuento</strong></li>\n<li class=\"\">Almacenamiento en bloque: <strong>$0.10/GB-mo</strong></li>\n</ul>\n<p>El modelo cosió un precio de almacenamiento a \"GPU\". ¿Por qué? Mira el trozo que le dimos. Nuestra\nextracción separada por espacios había aplastado las <em>tarjetas</em> de precios en una única línea\nindiferenciada:</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>La alucinación la fabricó nuestra propia ingesta.</strong> El modelo confundió precios porque destruimos\nla estructura antes de generar los embeddings. Este es el fallo RAG más clásico que existe — basura\nentra, basura sale — y solo lo cazas pinchando con preguntas y <em>leyendo los trozos recuperados</em>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La alucinación sutil — tres tarjetas de precio soldadas en una afirmación\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 5.</strong> El bug en libertad: negativa correcta, precios revueltos — una tarifa de\nalmacenamiento pegada a la opción de prepago de GPU. Las fuentes parecen perfectamente plausibles.\nPor esto \"cita fuentes\" no es lo mismo que \"está fundamentado\".</p>\n<p>Dos arreglos, los dos ya en el código de arriba:</p>\n<ol>\n<li class=\"\"><strong>Extracción consciente de la estructura</strong> — <code>get_text(\"\\n\", strip=True)</code> conserva los límites de\nlínea, así que las tarjetas de precio siguen siendo líneas separadas en vez de una sopa.</li>\n<li class=\"\"><strong>Un prompt de generación endurecido</strong> — cita los números literalmente con su unidad y su\nelemento; nunca combines líneas; nunca derives números nuevos de ejemplos. (Antes de esta regla,\nel modelo <em>calculaba</em> alegremente un falso \"$1.5834/GPU-hour\" a partir de un ejemplo de gasto\ndiario.)</li>\n</ol>\n<p>Después de volver a ingerir con el arreglo, la confusión con el precio de almacenamiento desaparece.\nQueda ambigüedad residual — la tira de precios todavía contiene fragmentos como \"$1.44/mo\" (un\nprecio de instancia de cómputo) cerca de la línea de GPU, y arreglar eso de verdad necesita\nextracción consciente de tablas (mira <em>Lo que viene</em>). Que es exactamente la razón por la que existe\nel paso siguiente: <strong>fijar el bug arreglado para que no pueda volver en silencio.</strong></p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5--evaluar-el-servicio-como-una-puerta-de-ci\">Paso 5 — Evaluar el servicio como una puerta de CI<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#paso-5--evaluar-el-servicio-como-una-puerta-de-ci\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 5 — Evaluar el servicio como una puerta de CI\" title=\"Enlace directo al Paso 5 — Evaluar el servicio como una puerta de CI\" translate=\"no\">​</a></h2>\n<p>Este paso necesitó <strong>cuatro rondas de arreglos para conseguir un número honesto</strong> — y el viaje del\n39% al 87% es la mejor lección de la serie, porque casi nada de lo que arreglamos por el camino era\nel RAG en sí.</p>\n<p>Reutilizamos la habilidad de la parte 3 — generar un conjunto de QA <em>desde el propio corpus</em> — y\ncalificamos el <strong>endpoint HTTP vivo</strong> con Promptfoo. La primera versión del generador pedía una\npregunta por página con un <code>key_fact</code> que la respuesta debía contener, y devolvió 37/37 pares.\nAlgunas etiquetas ya olían mal — un vago <code>\"All paid\"</code>, rutas de menú en vez de respuestas — pero\nvamos a ejecutarlo de forma naíf primero y dejar que los fallos nos enseñen. Convierte a un conjunto\nde datos de Promptfoo con aserciones de subcadena simples (<code>icontains:</code> + el hecho clave) y apúntalo\nal servicio en marcha. El proveedor es <strong>HTTP</strong> — calificamos la API real, exactamente lo que haría\nuna puerta de CI:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Ese 39,5% es lo que medimos <em>durante el desarrollo</em>, con el bug del Paso 4 todavía vivo. Vuelve a\nejecutar la misma eval naíf contra el servicio <strong>terminado</strong> y sale mucho más alto — 27/38:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La eval naíf contra el servicio terminado — 71%\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 6.</strong> La misma eval naíf contra el servicio terminado: <strong>71,05%</strong>. Mejor servicio, las mismas\netiquetas ruidosas — la diferencia de 30 puntos entre estas dos ejecuciones es toda calidad del\nservicio, y los dos números significan poco hasta que lees las filas.</p>\n<p><strong>Lee los fallos antes de entrar en pánico.</strong> De los 23 fallos de la ejecución de desarrollo, la\nmayoría no son fallos del RAG:</p>\n<table><thead><tr><th>Fallo</th><th>Qué pasó en realidad</th></tr></thead><tbody><tr><td>Respuesta de \"Balance due\": <em>\"the amount that remains due… $0.00\"</em></td><td>Respuesta correcta; la etiqueta exigía la frase <code>All paid</code> — <strong>etiqueta mala</strong></td></tr><tr><td>Payment Information: da la ruta de menú exacta y correcta</td><td>Correcto; la redacción de la etiqueta no coincide como subcadena — <strong>aserción frágil</strong></td></tr><tr><td>Precio de GPU: <em>\"$0.10 per GB-month\"</em></td><td><strong>Fallo real</strong> — la confusión del Paso 4, reproduciéndose sistemáticamente</td></tr><tr><td>Formato por defecto de TTS: <em>\"not specified in the context\"</em></td><td><strong>Fallo real de recuperación</strong> — el trozo con el valor por defecto no se recuperó</td></tr></tbody></table>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"mejora-1--un-juez-en-vez-de-subcadenas-apenas-ayuda\">Mejora 1 — un juez en vez de subcadenas. Apenas ayuda.<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#mejora-1--un-juez-en-vez-de-subcadenas-apenas-ayuda\" class=\"hash-link\" aria-label=\"Enlace directo al Mejora 1 — un juez en vez de subcadenas. Apenas ayuda.\" title=\"Enlace directo al Mejora 1 — un juez en vez de subcadenas. Apenas ayuda.\" translate=\"no\">​</a></h3>\n<p>La coincidencia de subcadenas (<code>icontains</code>) era perfecta para el único campo JSON de la parte 2 y es\n<strong>demasiado frágil para respuestas RAG largas</strong>. La mejora obvia es la habilidad de la parte 4: un\n<strong>juez LLM</strong> calificando la consistencia semántica (aserciones <code>llm-rubric:</code> + un modelo juez en\n<code>defaultTest</code>). Hicimos exactamente eso — las mismas 37 etiquetas, juez en vez de subcadenas — y\nobtuvimos:</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> Apenas se movió — porque <strong>el juez hace cumplir fielmente unas etiquetas malas.</strong>\nLas filas con <code>key_fact</code> basura siguen fallando: la referencia está mal, no la respuesta. Un\ncalificador mejor no puede rescatar un conjunto de datos malo. El cuello de botella nunca fue el\ntipo de aserción.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"mejora-2--curar-las-etiquetas-con-una-puerta\">Mejora 2 — curar las etiquetas con una puerta<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#mejora-2--curar-las-etiquetas-con-una-puerta\" class=\"hash-link\" aria-label=\"Enlace directo al Mejora 2 — curar las etiquetas con una puerta\" title=\"Enlace directo al Mejora 2 — curar las etiquetas con una puerta\" translate=\"no\">​</a></h3>\n<p>Así que arregla los datos (la disciplina de la parte 3, ahora impuesta por código). El generador v2\nexige que la etiqueta sea <em>la respuesta</em> — no una ruta de menú, no un nombre de sección — y añade una\n<strong>puerta de curación</strong>: el <code>key_fact</code> tiene que existir <strong>literalmente</strong> en el texto de origen, o la\nfila se rechaza en el sitio:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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 conservados, 22 rechazados.</strong> Esa tasa de rechazo es el punto: más de la mitad de lo que\nprodujo el generador habría calificado el RAG contra referencias equivocadas. Un conjunto de datos\nmás pequeño y fiable vence a uno más grande y envenenado.</p>\n<p>El generador muestrea con temperatura 0.3, así que el recuento exacto cambia ligeramente entre\nejecuciones — aquí una regeneración posterior que conservó 16 de 37; la tasa de rechazo sigue siendo\nbrutal en cualquier caso:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La puerta de curación — pares verificados literalmente conservados, el resto rechazados\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 7.</strong> La puerta en acción: 16 conservados, 21 rechazados en esta ejecución. Cada etiqueta\nque sobrevive está demostrablemente en la documentación — hechos-respuesta reales como <code>audio/mpeg</code> y\nla URL base de la API, no rutas de menú. (Las ejecuciones documentadas abajo usan nuestro primer\nconjunto curado: 15 pares + la regresión = 16 tests.)</p>\n<p>Reconstruye <code>tests.csv</code> con una rúbrica que le dé al juez la pregunta <strong>y</strong> la referencia verificada,\nmás reglas explícitas de PASS/FAIL (una redacción distinta está bien; una contradicción falla):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash 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> Mejor — pero antes de celebrarlo, lee los cuatro fallos. Y aquí el hábito de la serie paga\nuna vez más, porque <strong>tres de ellos tampoco eran fallos del RAG.</strong></p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"mejora-3--el-juez-mismo-estaba-rompiéndose\">Mejora 3 — el juez mismo estaba rompiéndose<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#mejora-3--el-juez-mismo-estaba-rompi%C3%A9ndose\" class=\"hash-link\" aria-label=\"Enlace directo al Mejora 3 — el juez mismo estaba rompiéndose\" title=\"Enlace directo al Mejora 3 — el juez mismo estaba rompiéndose\" translate=\"no\">​</a></h3>\n<p>Los tres fallos cuya razón de calificación era solo <em>\"No output\"</em> compartían dos señales:\n<code>graderError: true</code>, y una completación de <strong>exactamente 1024 tokens</strong> — el <code>max_tokens</code> por defecto\nde Promptfoo. Nuestro juez es un modelo que razona: gastó todo el presupuesto razonando y quedó\n<strong>cortado antes de emitir el JSON del veredicto</strong>, que Promptfoo cuenta como FAIL. El cuarto \"fallo\"\nsí obtuvo un veredicto — pero el juez pequeño había repetido literalmente el marcador del esquema\n(<code>reason: \"string\"</code>) en vez de calificar. Las dos respuestas eran de hecho correctas.</p>\n<p>Dos líneas de configuración lo arreglan — un juez más grande, y espacio para pensar:</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>Y la fila que más importa:</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>La regresión está en verde</strong> — la confusión del Paso 4 está fijada y no puede volver en silencio.\n(Una nota cosmética: el juez de 122B a veces reduce su texto de razón a un lacónico <code>...</code> — los\nveredictos en sí son correctos, que es lo que lee la puerta.)</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Sabes poner una puerta de CI a un servicio RAG vivo: QA generado y curado por una puerta, un juez\npara la semántica, regresiones deterministas para bugs conocidos — y sabes depurar al propio juez.</p></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"mejora-4--el-875-no-sobrevivió-a-una-repetición\">Mejora 4 — el 87,5% no sobrevivió a una repetición<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#mejora-4--el-875-no-sobrevivi%C3%B3-a-una-repetici%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al Mejora 4 — el 87,5% no sobrevivió a una repetición\" title=\"Enlace directo al Mejora 4 — el 87,5% no sobrevivió a una repetición\" translate=\"no\">​</a></h3>\n<p>Un hábito que esta serie machaca: <strong>vuelve a ejecutar antes de creer.</strong> Lo hicimos — y obtuvimos\n<strong>75%</strong>, con filas <em>distintas</em> fallando que antes. Facturación, que acababa de pasar, ahora falló\ncon un eco circular; la respuesta de TTS decía \"mp3\" en una ejecución y \"sin especificar\" en la\nsiguiente. La puntuación era una lotería porque <strong>el servicio mismo era no determinista</strong>:\n<code>generate()</code> no pasaba <code>temperature</code>, así que cada eval calificaba una tirada distinta de dados con\nel valor por defecto de 1.0.</p>\n<p>Una línea acaba con la lotería — añádela a la llamada a <code>create()</code> en <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>Vuelve a ejecutar dos veces. <strong>13/16 — 81,25% — las dos veces, con los mismos tres fallos.</strong> El\nnúmero <em>bajó</em> y eso es la victoria: el 87,5% era en parte suerte (las filas inestables cayeron bien\nen esa ejecución); el 81,25% es reproducible. Alguien que repita tu eval debería obtener tu número.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"los-tres-fallos-reales--por-fin-bugs-de-rag-de-verdad\">Los tres fallos reales — por fin, bugs de RAG de verdad<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#los-tres-fallos-reales--por-fin-bugs-de-rag-de-verdad\" class=\"hash-link\" aria-label=\"Enlace directo al Los tres fallos reales — por fin, bugs de RAG de verdad\" title=\"Enlace directo al Los tres fallos reales — por fin, bugs de RAG de verdad\" translate=\"no\">​</a></h3>\n<p>Después de cuatro rondas arreglando la <em>medición</em>, lo que queda es señal — estable entre\nejecuciones, y cada uno es un <strong>tipo clásico de fallo RAG</strong> distinto:</p>\n<ol>\n<li class=\"\"><strong>Subredes (\"Total Networks\") — fallo de recuperación, límite de trozo.</strong> La página correcta\naparece en el ranking, pero si vuelcas los cuatro trozos recuperados ninguno contiene el bloque de\nestadísticas con la respuesta; el trozo de arriba empieza <strong>a mitad de palabra</strong>\n(<code>\"iated with subnets\\nData Transfer…\"</code>). El troceado de tamaño fijo por caracteres partió el\nhecho por un límite, y la mitad con la respuesta no aparece en el ranking. Clase de arreglo: más\nsolapamiento, <code>k</code> mayor, o troceado consciente de la estructura.</li>\n<li class=\"\"><strong>Formato por defecto de TTS — fallo de recuperación, el hecho no está en el top-k.</strong> El trozo\nque declara el valor por defecto nunca llega al contexto. Detalle revelador: con temperatura 1.0\nesta fila <em>a veces pasaba</em> — el modelo adivinaba \"mp3\" sin evidencia. El determinismo convirtió\nuna adivinanza afortunada en un fallo honesto y consistente.</li>\n<li class=\"\"><strong>Facturación (\"How Billing Works\") — calidad de generación.</strong> A temperatura 0 el modelo pequeño\nrepite deterministamente la frase de origen — <em>\"This is the starting point for billing…\"</em> — sin\nnombrar la página. Recuperación correcta sin negativa, respuesta circular e inútil; el juez la\nfalla con razón. Clase de arreglo: trabajo de prompt (\"nombra la página o sección que estás\ncitando\") o un modelo de generación más fuerte.</li>\n</ol>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"gasta-la-señal--un-parámetro-elegido-por-la-eval\">Gasta la señal — un parámetro, elegido por la eval<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#gasta-la-se%C3%B1al--un-par%C3%A1metro-elegido-por-la-eval\" class=\"hash-link\" aria-label=\"Enlace directo al Gasta la señal — un parámetro, elegido por la eval\" title=\"Enlace directo al Gasta la señal — un parámetro, elegido por la eval\" translate=\"no\">​</a></h3>\n<p>Una eval fiable no es la meta; es el <em>mapa</em>. Acaba de decirnos que dos de tres fallos son de la misma\nclase — hechos que no llegan a los cuatro trozos recuperados. El arreglo más barato de esa clase es\nun carácter, en <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>Vuelve a ejecutar. Y luego otra vez, porque ya es el hábito:</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% — dos veces, los mismos dos fallos.</strong> Lee qué pasó con cada uno:</p>\n<ul>\n<li class=\"\"><strong>TTS por defecto — arreglado.</strong> Con seis trozos la página que declara el valor por defecto llega\nal contexto, y el servicio ahora responde \"mp3\" <em>con evidencia</em>, en cada ejecución — la adivinanza\nafortunada se convirtió en una respuesta fundamentada.</li>\n<li class=\"\"><strong>Subredes — sobrevivió.</strong> Incluso con <code>k=6</code> el trozo con el bloque de estadísticas no aparece en\nel ranking: este fallo es <em>semántico</em>, no de presupuesto — el texto destrozado por el límite\n(<code>\"iated with subnets…\"</code>) genera embeddings pobres. Más <code>k</code> no puede arreglar lo que el troceado\nrompió; este necesita de verdad troceado consciente de la estructura.</li>\n<li class=\"\"><strong>Facturación — sobrevivió, como estaba previsto.</strong> Es un fallo de calidad de generación; ningún\nparámetro de recuperación iba a tocarlo nunca.</li>\n</ul>\n<p>El número vuelve a 87,5% — el mismo que la ejecución afortunada — <strong>pero esta vez se reproduce</strong>, y\ncada punto tiene una explicación.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Eval final — servicio determinista, k=6, calificado por juez, regresión pasando\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 8.</strong> La ejecución final: 14/16, los mismos dos fallos cada vez, regresión en PASS.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lee-el-número-final-con-honestidad\">Lee el número final con honestidad<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#lee-el-n%C3%BAmero-final-con-honestidad\" class=\"hash-link\" aria-label=\"Enlace directo al Lee el número final con honestidad\" title=\"Enlace directo al Lee el número final con honestidad\" translate=\"no\">​</a></h3>\n<p>39,5% → 44,7% → 75% → 87,5% <em>(con suerte)</em> → 81,25% <em>(honesto)</em> → <strong>87,5% (ganado)</strong>. Hasta el\núltimo paso, el RAG apenas cambió — lo que cambió fue la <strong>medición</strong>: las aserciones (subcadenas →\njuez), las etiquetas (puerta de curación), la configuración del propio juez (truncamiento), el\ndeterminismo (temperatura). Solo entonces un parámetro de recuperación, elegido <em>por</em> la eval, movió\nla calidad de verdad. Esa es la última lección de la serie:</p>\n<ul>\n<li class=\"\"><strong>La mayoría de los \"fallos\" eran nuestra eval mintiéndonos</strong> — etiquetas malas, aserciones\nfrágiles, un juez truncado en silencio, un servicio tirando dados. Lee las filas antes de tocar el\nRAG.</li>\n<li class=\"\"><strong>El número honesto era más bajo que el afortunado — y vale más</strong>, porque se reproduce. Si tu\npuntuación cambia cuando repites, todavía no tienes una puntuación.</li>\n<li class=\"\"><strong>Arregla la medición primero, luego gasta su señal.</strong> El mismo 87,5% aparece dos veces en este\narco; solo el segundo significa algo — sobrevive a las repeticiones y cada punto tiene una\nexplicación.</li>\n<li class=\"\"><strong>La puntuación es un instrumento de tendencia; las filas son la verdad.</strong> El 87,5% todavía no es\n\"la calidad del RAG\" — es este conjunto de datos, este juez, este corpus. Fija lo que nunca debe\nregresar con aserciones deterministas; deja que el juez siga el resto.</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-has-construido\">Lo que has construido<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#lo-que-has-construido\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que has construido\" title=\"Enlace directo al Lo que has construido\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">Un <strong>servicio RAG con Docker primero</strong>: extraer (sitemap → extracción del artículo) → trocear →\n<strong>embeddings locales</strong> (fastembed/ONNX) → Chroma → <strong>generación en WEC</strong>, detrás de <code>POST /ask</code>\ncon citas deterministas.</li>\n<li class=\"\">Una <strong>historia de bug real</strong>: pinchamos el caso negativo, cazamos un precio de GPU revuelto (una\ntarifa de almacenamiento soldada a la opción de prepago de GPU), encontramos la causa en la\nestructura aplastada del scraper, arreglamos la ingesta y endurecimos el prompt, y <strong>lo fijamos\ncon un test de regresión que ahora pasa</strong>.</li>\n<li class=\"\">Una <strong>eval con forma de CI</strong> del endpoint vivo: QA curado por una puerta (15 pares verificados\nliteralmente de 37 generados) + un juez que depuraste como cualquier otro componente — 39,5% →\n<strong>87,5% reproducible</strong>, arreglando primero la <em>medición</em> (aserciones, etiquetas, configuración del\njuez, determinismo) y luego gastando su señal en un parámetro de recuperación elegido por la eval.\nQuedan dos fallos reales y nombrables — a propósito, con sus clases de arreglo nombradas.</li>\n<li class=\"\"><strong>Observabilidad completa</strong>: cada petición un árbol de spans en Langfuse — recuperación y\ngeneración depurables por separado.</li>\n</ul>\n<p>Las partes 1 a 4 eran la caja de herramientas. Esto es para lo que <em>sirve</em> la caja de herramientas.</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=\"resolución-de-problemas\">Resolución de problemas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#resoluci%C3%B3n-de-problemas\" class=\"hash-link\" aria-label=\"Enlace directo al Resolución de problemas\" title=\"Enlace directo al Resolución de problemas\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"ask-no-devuelve-nada-después-de-reindexar\"><code>/ask</code> no devuelve nada después de reindexar<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#ask-no-devuelve-nada-despu%C3%A9s-de-reindexar\" class=\"hash-link\" aria-label=\"Enlace directo al ask-no-devuelve-nada-después-de-reindexar\" title=\"Enlace directo al ask-no-devuelve-nada-después-de-reindexar\" translate=\"no\">​</a></h3>\n<p>Si borras y recreas la colección de Chroma mientras la API está en marcha, el servicio conserva un\n<strong>handle obsoleto de la colección</strong> y cada consulta lanza:</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>El proceso en marcha guardó en caché el UUID de la colección antigua. Reinicia el servicio\n(<code>docker compose restart rag-api</code>) — o resuelve la colección por petición en vez de al arrancar.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El error de colección obsoleta después de reindexar\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 9.</strong> Reindexar invalida los handles abiertos — una trampa clásica de producción del tipo\n\"funciona hasta que reconstruyes el índice\".</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"failed-to-send-telemetry-event--capture-takes-1-positional-argument\"><code>Failed to send telemetry event … capture() takes 1 positional argument</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#failed-to-send-telemetry-event--capture-takes-1-positional-argument\" class=\"hash-link\" aria-label=\"Enlace directo al failed-to-send-telemetry-event--capture-takes-1-positional-argument\" title=\"Enlace directo al failed-to-send-telemetry-event--capture-takes-1-positional-argument\" translate=\"no\">​</a></h3>\n<p>Choque inofensivo de versiones entre chromadb y posthog. Pon <code>ANONYMIZED_TELEMETRY: \"False\"</code> en el\nentorno (ya está en nuestro compose).</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"fallos-del-juez-con-razón-no-output-y-gradererror-true\">Fallos del juez con razón \"No output\" (y <code>graderError: true</code>)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#fallos-del-juez-con-raz%C3%B3n-no-output-y-gradererror-true\" class=\"hash-link\" aria-label=\"Enlace directo al fallos-del-juez-con-razón-no-output-y-gradererror-true\" title=\"Enlace directo al fallos-del-juez-con-razón-no-output-y-gradererror-true\" translate=\"no\">​</a></h3>\n<p>Comprueba los tokens de completación del registro de calificación: si son <strong>exactamente 1024</strong> — el\n<code>max_tokens</code> por defecto de Promptfoo — tu juez con razonamiento gastó todo el presupuesto razonando\ny quedó cortado antes del JSON del veredicto. Pon <code>max_tokens: 4096</code> en el proveedor del juez. Señal\nrelacionada: un juez pequeño devolviendo <code>reason: \"string\"</code> está repitiendo el marcador del esquema\nen vez de calificar — usa un juez más grande.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-eval-con-juez-es-lenta-o-los-veredictos-parecen-aleatorios\">La eval con juez es lenta o los veredictos parecen aleatorios<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#la-eval-con-juez-es-lenta-o-los-veredictos-parecen-aleatorios\" class=\"hash-link\" aria-label=\"Enlace directo al La eval con juez es lenta o los veredictos parecen aleatorios\" title=\"Enlace directo al La eval con juez es lenta o los veredictos parecen aleatorios\" translate=\"no\">​</a></h3>\n<p>Los jueces con razonamiento son lentos (~minutos para una docena de filas) e imperfectos — espera\ncambios de veredicto ocasionales entre ejecuciones. Mantén <strong>aserciones deterministas para bugs\nconocidos</strong> (son gratis y nunca cambian) y trata la puntuación del juez como una tendencia, no como\nuna verdad. Si una fila del juez se cuelga del todo, normalmente es un hipo transitorio de red y no\ntu configuración — reintenta antes de depurar.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-build-de-docker-falla-con-no-space-left-on-device\">La build de Docker falla con \"no space left on device\"<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#la-build-de-docker-falla-con-no-space-left-on-device\" class=\"hash-link\" aria-label=\"Enlace directo al La build de Docker falla con &quot;no space left on device&quot;\" title=\"Enlace directo al La build de Docker falla con &quot;no space left on device&quot;\" translate=\"no\">​</a></h3>\n<p>La imagen necesita ~1 GB (chromadb y onnxruntime son las capas pesadas). Comprueba <code>df -h /</code> primero\ny <code>docker system df</code> para ver espacio reclamable — pero <strong>nunca hagas prune a ciegas en una máquina\ncompartida</strong>: \"reclamable\" miente cuando hay contenedores de otros proyectos en marcha.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-viene\">Lo que viene<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#lo-que-viene\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que viene\" title=\"Enlace directo al Lo que viene\" translate=\"no\">​</a></h2>\n<p>Las limitaciones honestas son la hoja de ruta, y cada una viene de un fallo que viste ocurrir:\n<strong>troceado consciente de la estructura</strong> (el fallo de subredes sobrevivió a <code>k=6</code> — el trozo\ndestrozado por el límite genera embeddings pobres, así que ningún presupuesto de recuperación lo\nrescata), <strong>trabajo de prompt o de modelo en la generación</strong> (el eco de facturación: obliga al\nmodelo a nombrar la página que cita, o usa un generador más fuerte), <strong>extracción consciente de\ntablas</strong> (la tira de precios sigue siendo sopa de fragmentos — la regresión pasa, pero \"$1.44/mo\"\ntodavía se filtra cerca de la línea de GPU; añade una aserción <em>positiva</em> para la cifra correcta, no\nsolo <code>not-icontains</code> para las equivocadas), y <strong>calibración del juez</strong> (califica al juez contra un\npuñado de veredictos humanos). Cada una mueve la tasa de aprobados por una razón que puedes nombrar\n— que, después de cinco partes, es el punto entero: <em>no adivinas si tu IA funciona. La mides, la\nobservas, y la haces demostrarlo.</em></p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lecturas-adicionales\">Lecturas adicionales<a href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/#lecturas-adicionales\" class=\"hash-link\" aria-label=\"Enlace directo al Lecturas adicionales\" title=\"Enlace directo al Lecturas adicionales\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.trychroma.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Documentación de Chroma — cliente persistente y colecciones</a></li>\n<li class=\"\"><a href=\"https://github.com/qdrant/fastembed\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">fastembed — embeddings ONNX sin PyTorch</a></li>\n<li class=\"\"><a href=\"https://www.promptfoo.dev/docs/providers/http/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Promptfoo — proveedor HTTP</a></li>\n<li class=\"\"><a href=\"https://langfuse.com/docs/sdk/python/decorators\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Langfuse — decoradores de Python</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/",
            "title": "El proyecto final: construye, evalúa y observa un asistente de documentación RAG en la API de WEC",
            "summary": "Construye un servicio RAG con forma de producción en Docker: extrae un sitio de documentación real, genera embeddings localmente, genera texto en la API de Inferencia de WEC — y luego caza una alucinación real, encuentra su causa en tu propio scraper, la arregla y la fija con un test de regresión. Cada comando, número y error es 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",
                "inferencia"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/",
            "content_html": "<div class=\"tutorialHero\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2255\" height=\"527\" fill=\"none\" viewBox=\"0 0 2255 527\" class=\"tutorialHero__langfuse\"><path fill=\"#1B1917\" d=\"M652.669 116.433c0-10.261-7.683-17.956-17.926-17.956H607V60h87.923v316.366h-42.254zM804.056 379.786c-39.693 0-72.131-27.362-72.131-68.831 0-41.042 28.596-69.686 84.935-69.686h39.693c7.256 0 12.805-5.558 12.805-12.826v-8.55c0-25.651-20.487-38.905-40.547-38.905-18.353 0-33.291 8.123-41.828 26.934h-45.668c11.95-44.462 46.095-65.41 88.349-65.41 40.547 0 82.374 22.231 82.374 77.808v156.046h-42.68v-14.109c0-5.13-5.549-7.695-9.817-4.702-16.646 11.97-32.438 22.231-55.485 22.231m5.975-38.477c18.78 0 34.572-9.406 52.498-27.362 4.695-4.702 6.829-10.688 6.829-17.1v-6.413c0-7.268-5.549-12.826-12.805-12.826H815.58c-27.743 0-40.974 12.398-40.974 31.637 0 17.956 12.377 32.064 35.425 32.064M961.855 376.366V147.642h42.255v19.666c0 5.13 6.4 6.84 10.24 2.992 13.23-12.825 31.59-27.788 60.61-27.788 36.71 0 70.85 23.086 70.85 75.671v158.183h-42.25V227.161c0-29.499-17.93-45.745-39.7-45.745-20.91 0-35 11.971-49.93 29.499-7.26 8.978-9.82 19.238-9.82 30.354v135.097zM1287.48 467c-52.5 0-85.79-24.796-95.61-63.273h46.1c7.25 15.818 20.06 25.651 45.67 25.651 34.14 0 55.48-20.093 55.48-65.838v-7.268c0-5.13-4.27-7.695-9.39-3.42-14.08 12.398-32.01 20.093-49.08 20.093-58.05 0-96.46-44.889-96.46-115.003 0-70.113 44.39-115.43 98.17-115.43 15.79 0 30.3 4.275 44.38 14.963 5.98 4.275 12.38.855 12.38-5.986v-3.847h42.26V363.54c0 72.678-43.97 103.46-93.9 103.46m-2.56-132.959c19.2 0 33.29-8.55 44.81-20.949 7.26-8.122 9.39-13.68 9.39-26.506v-61.563c0-12.826-2.13-20.948-10.67-29.071-9.39-8.978-22.62-14.964-39.69-14.964-35 0-61.89 29.072-61.89 76.954 0 47.883 24.76 76.099 58.05 76.099M1455.92 199.372c0-7.268-5.97-13.253-13.23-13.253h-32.44v-38.477h32.44c7.26 0 13.23-5.985 13.23-13.253v-5.986c0-45.744 23.48-68.403 69.15-68.403h29.02v38.477h-29.45c-17.5 0-26.46 9.833-26.46 29.926v5.986c0 7.268 5.97 13.253 13.23 13.253h42.68v38.477h-42.68c-7.26 0-13.23 5.985-13.23 13.253v176.994h-42.26zM1652.02 381.496c-35.85 0-69.14-23.086-69.14-75.671V147.642h42.25v150.06c0 29.499 17.07 44.889 37.13 44.889 21.77 0 35.85-11.97 50.79-29.499 7.26-8.977 9.82-19.238 9.82-30.354V147.642h42.25v228.724h-42.25V356.7c0-5.131-6.4-6.841-10.24-2.993-13.24 12.826-31.59 27.789-60.61 27.789M1893.57 381.496c-38.84 0-79.39-19.239-90.06-65.838h43.54c6.4 17.528 23.9 29.498 44.81 29.498 23.05 0 37.13-13.68 37.13-30.353 0-16.246-11.09-25.224-28.59-30.354l-36.28-10.261c-31.58-8.978-55.06-29.499-55.06-64.556 0-38.049 35.43-67.12 75.55-67.12 32.01 0 70.85 14.535 81.09 65.41h-40.55c-5.55-17.528-20.06-29.071-40.54-29.071-20.06 0-34.58 12.398-34.58 28.216 0 13.253 8.11 23.086 27.32 28.644l34.15 9.833c32.43 9.406 58.47 29.072 58.47 66.693 0 39.332-34.15 69.259-76.4 69.259M2098.54 381.496c-61.46 0-102.01-51.73-102.01-119.706s43.11-119.278 101.58-119.278c63.6 0 96.89 51.302 96.89 109.872v23.087h-144.26c-5.98 0-8.54 3.847-7.26 13.68 4.7 32.064 30.31 54.295 55.49 54.295 18.78 0 35-9.405 45.67-27.788h44.81c-16.22 40.187-49.94 65.838-90.91 65.838m43.11-141.51c6.83 0 9.39-3.42 7.68-14.108-4.69-26.506-24.33-45.317-51.22-45.317-25.6 0-47.37 18.811-54.2 45.745-2.56 9.833.85 13.68 6.83 13.68z\"></path><path fill=\"#FF5D5F\" d=\"m286.292 286.105 34.597 27.791s26.473-19.661 45.941-22.545c20.418-3.025 42.202 8.359 62.388 21.93 30.489 20.498 56.149 46.508 56.149 46.508l30.06-29.493s-82.879-89.795-148.597-81.672c-43.105 5.328-80.538 37.481-80.538 37.481\"></path><path fill=\"#4E9CFF\" d=\"M88.358 114.862 60 146.056s79.009 73.732 141.224 73.732c28.358 0 67.684-22.216 101.523-51.079 19.283-16.448 40.835-35.13 62.388-35.13 14.487 0 33.594 7.673 51.612 27.824 0 0 11.63-6.974 18.716-11.985 6.228-4.404 15.479-11.91 15.479-11.91-25.918-27.663-63.407-47.883-85.807-45.9-36.299.005-62.388 22.601-94.717 48.735s-45.94 36.907-69.194 36.907c-39.134 0-112.866-62.388-112.866-62.388M88.358 352.463 60 321.269s79.009-73.732 141.224-73.732c28.358 0 67.684 22.216 101.523 51.079 19.283 16.448 40.835 35.13 62.388 35.13 14.556 0 33.518-7.989 51.612-28.358 0 0 10.877 6.705 17.582 11.344 6.894 4.769 17.015 12.655 17.015 12.655-25.931 27.883-63.693 48.323-86.209 46.33-36.299-.005-57.851-19.24-90.179-45.374-32.329-26.133-50.478-40.268-73.732-40.268-39.134 0-112.866 62.388-112.866 62.388M458.142 185.149c-7.378 5.1-19.283 12.478-19.283 12.478s6.806 14.746 6.806 34.597-6.239 36.866-6.239 36.866 10.688 6.675 17.582 11.343c7.162 4.849 18.149 13.045 18.149 13.045s13.045-27.224 13.045-61.254-13.045-59.552-13.045-59.552-10.236 7.792-17.015 12.477\"></path><path fill=\"#FF5D5F\" d=\"m287.995 180.612 32.895-27.224s26.473 19.046 45.941 21.93c20.417 3.026 42.202-8.359 62.388-21.93 30.489-20.498 56.149-46.507 56.149-46.507l30.06 29.492s-82.879 89.795-148.597 81.672c-43.105-5.328-78.836-37.433-78.836-37.433M208.601 91c42.538 0 78.264 36.299 78.264 36.299s-9.941 7.832-16.448 13.045c-6.777 5.429-17.582 14.179-17.582 14.179s-18.711-19.851-44.234-19.851c-10.465 0-24.066 6.286-38.567 18.716-11.188 9.591-22.829 21.514-30.627 36.299-6.743 12.784-10.42 27.85-10.776 43.672-.446 19.873 6.597 40.704 18.149 57.283 7.743 11.112 16.983 19.474 26.657 26.657 12.555 9.322 25.648 15.881 35.164 15.881 10.166 0 19.306-3.533 26.09-6.806 10.776-6.239 19.278-13.612 19.278-13.612l33.463 27.791s-13.612 13.612-32.323 23.821c-12.091 5.963-27.632 11.91-46.508 11.91-18.862 0-40.767-10.022-61.254-25.522-13.244-10.021-26.225-21.895-36.298-36.299-16.51-23.607-25.017-52.328-24.96-81.104.057-29.136 9.451-57.993 26.094-81.672C138.273 117.657 176.86 91 208.601 91\"></path></svg><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 8 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->8</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->8<!-- --> earned</span></div><div class=\"skillTracker__series\">AI evals &amp; observability</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove a model works</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trustworthy JSON</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real test data at scale</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">4</span><span class=\"skillTracker__skill\" data-state=\"current\">Observe &amp; score production</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">RAG, end to end</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">Catch regressions in CI</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trace &amp; debug agent tool calls</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/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>Un cliente dice que tu bot de soporte les prometió una política de reembolso que no existe. Tu función\nhizo <strong>dos</strong> llamadas LLM: clasificar y luego responder. ¿Cuál de ellas la inventó? Si no puedes responder eso,\ntu aplicación es una caja negra — y esta guía soluciona exactamente eso.</p>\n<p>Ahora puedes probar que un modelo funciona (<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/\">parte 1</a>),\nhacer que su salida sea confiable para máquinas (<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/\">parte 2</a>),\ny generar un conjunto de pruebas real para verificarlo\n(<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/\">parte 3</a>). Pero todo eso se ejecuta <strong>fuera de línea,\nen CI, con entradas que elegiste.</strong> La producción no juega de esa manera.</p>\n<p>Esta guía cierra la brecha. Autoalojaremos <strong>Langfuse</strong> — la alternativa de código abierto y autoalojable\na LangSmith — rastrearemos cada llamada real, <strong>evaluaremos automáticamente el tráfico en vivo con un juez LLM</strong>,\nprofundizaremos en el paso exacto que falla y <strong>retroalimentaremos los fallos</strong> para que tu conjunto de datos de la parte 3 se vuelva\nmás fuerte. La evaluación fuera de línea te dice que funcionó en tu conjunto de pruebas; esto te dice que funciona en el mundo real.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-habilidad-que-esto-añade\">La habilidad que esto añade<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#la-habilidad-que-esto-a%C3%B1ade\" class=\"hash-link\" aria-label=\"Enlace directo al La habilidad que esto añade\" title=\"Enlace directo al La habilidad que esto añade\" translate=\"no\">​</a></h2>\n<p>Al final podrás tomar cualquier función WEC y: <strong>ver dentro de cada llamada, evaluar la calidad en tráfico real automáticamente, encontrar la llamada exacta que falló y por qué, y convertir los fallos de producción en nuevos casos de prueba.</strong> Esa es la diferencia entre \"pasó CI\" y \"se mantiene en producción.\"</p>\n<p>Mantendremos el ejemplo en ejecución de la parte 3 — un <strong>clasificador de tickets de soporte</strong> en la API de Inferencia WEC — y lo pondremos bajo observación.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-estamos-construyendo\">Lo que estamos construyendo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#lo-que-estamos-construyendo\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que estamos construyendo\" title=\"Enlace directo al Lo que estamos construyendo\" translate=\"no\">​</a></h2>\n<p>Tu aplicación llama a la API WEC como de costumbre; Langfuse (autoalojado) captura silenciosamente <strong>cada llamada como un\nrastreo</strong>, un juez LLM <strong>evalúa</strong> cada una, y los fallos fluyen <strong>de vuelta a tu conjunto de datos de la parte 3</strong> — para que la producción siga fortaleciendo tus evaluaciones:</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"por-qué-langfuse-y-cuándo-elegir-algo-más-ligero\">Por qué Langfuse (y cuándo elegir algo más ligero)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#por-qu%C3%A9-langfuse-y-cu%C3%A1ndo-elegir-algo-m%C3%A1s-ligero\" class=\"hash-link\" aria-label=\"Enlace directo al Por qué Langfuse (y cuándo elegir algo más ligero)\" title=\"Enlace directo al Por qué Langfuse (y cuándo elegir algo más ligero)\" translate=\"no\">​</a></h2>\n<p>Usamos Langfuse porque es el líder de código abierto en observabilidad LLM (licencia MIT,\nautoalojable), su SDK es <strong>compatible con OpenAI</strong> por lo que apunta a la API WEC sin cambios, y — la\nrazón por la que encaja en <em>esta</em> serie — tiene <strong>conjuntos de datos, evaluación y evaluadores LLM-como-juez integrados.</strong>\nEso permite que tu conjunto de datos de la parte 3 y el bucle de \"evaluar producción, retroalimentar fallos\" sean\nde primera clase en lugar de añadidos.</p>\n<p>La desventaja: la pila de producción es pesada (Postgres + ClickHouse). Si no necesitas las características de evaluación\ny quieres algo más ligero, alternativas razonables son <strong>Arize Phoenix</strong> (nativa de OpenTelemetry,\nfuerte para evaluación fuera de línea), <strong>Helicone</strong> (un proxy de una línea — pero sin evaluaciones), o <strong>OpenLIT</strong>\n(un solo binario, nativa de OTEL). Para un flujo de trabajo impulsado por evaluaciones como este, los contenedores adicionales valen la pena; para un simple rastreo, uno de esos puede ser más adecuado para ti.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--autoalojar-langfuse-en-una-instancia-wec\">Paso 1 — Autoalojar Langfuse en una Instancia WEC<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#paso-1--autoalojar-langfuse-en-una-instancia-wec\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Autoalojar Langfuse en una Instancia WEC\" title=\"Enlace directo al Paso 1 — Autoalojar Langfuse en una Instancia WEC\" translate=\"no\">​</a></h2>\n<p>La pila autoalojada de Langfuse consta de seis servicios (web + worker, ClickHouse, Postgres, Redis, almacenamiento de objetos), así que dale una Instancia WEC con <strong>~15 GB de disco y 4 GB+ de RAM</strong> — no lo metas en una máquina que ya esté ocupada.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Genera tres secretos y colócalos en un <code>.env</code> (la <code>ENCRYPTION_KEY</code> <strong>debe</strong> ser hex, o el contenedor web no arrancará):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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;tu-ip-de-instancia&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=\"La pila de Langfuse en funcionamiento — seis servicios saludables\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 1.</strong> <code>docker compose ps</code> — los seis servicios (web, worker, ClickHouse, Postgres, Redis, MinIO) en funcionamiento y saludables.</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>Desplegando junto a otros servicios</div><div class=\"admonitionContent_BuS1\"><p>Si la máquina ya ejecuta algo en el puerto 3000 o 5432, Langfuse no arrancará (\"dirección ya en uso\"). Las bases de datos de respaldo no necesitan puertos de host en absoluto — edita <code>docker-compose.yml</code> para mover <code>langfuse-web</code> a un puerto de host libre (por ejemplo, <code>3001:3000</code>) y elimina el mapeo del host de Postgres. Mantener tus bases de datos fuera de la red del host es una buena práctica de todos modos. Consulta <a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#ts-ports\" class=\"\">Solución de problemas</a>.</p></div></div>\n<p>Abre <code>http://&lt;ip-de-instancia&gt;:3000</code>, crea una cuenta (el primer usuario es el propietario), luego una\n<strong>Organización → Proyecto</strong>, y bajo <strong>Configuración → Claves API</strong> crea un par de claves\n(<code>pk-lf-…</code> / <code>sk-lf-…</code>).</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--instrumentar-el-clasificador-para-que-cada-llamada-sea-trazada\">Paso 2 — Instrumentar el clasificador para que cada llamada sea trazada<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#paso-2--instrumentar-el-clasificador-para-que-cada-llamada-sea-trazada\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — Instrumentar el clasificador para que cada llamada sea trazada\" title=\"Enlace directo al Paso 2 — Instrumentar el clasificador para que cada llamada sea trazada\" translate=\"no\">​</a></h2>\n<p>Apunta el SDK de Langfuse a tu instancia y usa su <strong>OpenAI drop-in</strong> — traza automáticamente cualquier\nllamada compatible con OpenAI, incluida la API de Inferencia WEC, sin código adicional alrededor de cada solicitud:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_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;ip-de-instancia&gt;:3000\"</span><span class=\"token plain\">   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># usa el puerto que expusiste — 3001 si lo remapeaste en el Paso 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-tu-clave-wec\"</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-traza cada llamada</span><span class=\"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\"># SIEMPRE establece esto — un backend lento cuelga indefinidamente de lo contrario</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\"># nombra el rastreo en 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\">\"Clasifica este ticket de soporte. Devuelve SOLO JSON con claves \"</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\">\"categoría (facturación/técnico/cuenta/otro), prioridad (baja/media/alta), \"</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\"resumen. 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\">\"Me cobraron dos veces este mes.\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"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\"># empuja los rastros antes de que el proceso termine</span><br></div></code></pre></div></div>\n<p>Ejecuta el código, luego abre <strong>Trazado</strong> — la llamada aparecerá en segundos con entrada, salida, latencia, conteos de tokens, y (una vez que agreguemos precios) costo.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Un solo rastreo — entrada, salida, latencia y conteos de tokens\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 2.</strong> Una llamada <code>ticket-classify</code> en Langfuse: el prompt, la salida JSON, latencia (~1.2s),\ny conteos de tokens — capturados sin código adicional alrededor de la solicitud.</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>Establecer un tiempo de espera</div><div class=\"admonitionContent_BuS1\"><p>El cliente de OpenAI <strong>no tiene un tiempo de espera predeterminado</strong>. Si el modelo es lento o el backend se detiene, tu\naplicación se cuelga indefinidamente — sin error, sin rastreo. <code>timeout=30</code> convierte eso en un fallo rápido y visible.</p></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"de-una-llamada-a-un-pipeline-real--el-árbol-de-spans\">De una llamada a un pipeline real — el árbol de spans<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#de-una-llamada-a-un-pipeline-real--el-%C3%A1rbol-de-spans\" class=\"hash-link\" aria-label=\"Enlace directo al De una llamada a un pipeline real — el árbol de spans\" title=\"Enlace directo al De una llamada a un pipeline real — el árbol de spans\" translate=\"no\">​</a></h3>\n<p>Una llamada es fácil de registrar; no necesitas Langfuse para eso. El rastreo se justifica en funciones <strong>de múltiples pasos</strong>,\ndonde muestra <strong>toda la cadena como un árbol</strong> para que puedas ver <em>qué paso</em> falló. Hagamos que\nla función sea realista — <strong>clasifica el ticket, luego redacta una respuesta de esa categoría</strong> (dos llamadas WEC) —\ny agrúpalas en un solo rastreo con el decorador <code>@observe</code>:</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\"># reutiliza el cliente WEC trazado + clasificador</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\"Un cliente envió este ticket </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\">. Escribe una respuesta corta y útil.\\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\"># agrupa todo lo que está debajo en UN rastreo</span><span class=\"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\"># span hijo 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\"># span hijo 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\">\"Me cobraron dos veces este mes.\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"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>Abre ese rastreo y obtendrás un <strong>árbol</strong>: <code>handle_ticket</code> → <code>ticket-classify</code> → <code>draft-reply</code>, cada\nhijo con su propia entrada, salida, latencia y tokens. Ahora un fallo tiene una <strong>dirección</strong> — puedes\napuntar al paso exacto que falló en lugar de adivinar (usamos esto en el Paso 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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Ahora puedes <strong>ver dentro de cualquier llamada LLM en producción</strong> — entrada, salida, latencia, tokens — sin\nescribir una línea de código de registro.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--reproducir-tu-conjunto-de-datos-de-la-parte-3-a-través-de-él\">Paso 3 — Reproducir tu conjunto de datos de la parte 3 a través de él<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#paso-3--reproducir-tu-conjunto-de-datos-de-la-parte-3-a-trav%C3%A9s-de-%C3%A9l\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — Reproducir tu conjunto de datos de la parte 3 a través de él\" title=\"Enlace directo al Paso 3 — Reproducir tu conjunto de datos de la parte 3 a través de él\" translate=\"no\">​</a></h2>\n<p>Para obtener una señal real (no una llamada), empuja tu <strong>conjunto de datos de la parte 3</strong> a través del clasificador para que\nel tablero tenga tráfico que analizar:</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\"># reutiliza la función trazada</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>Ahora el <strong>Tablero de inicio</strong> muestra rastros a lo largo del tiempo, una distribución de latencia (p50/p95), tokens por\nmodelo, y un total de costos — la vista de \"observa en producción\".</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El tablero de Langfuse poblado con tráfico reproducido\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 3.</strong> Después de reproducir el conjunto de datos de la parte 3: rastros a lo largo del tiempo, una distribución de latencia, tokens\npor modelo, y un total de costos — la vista de \"observa en producción\".</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"hacer-que-el-costo-sea-real-para-los-modelos-wec\">Hacer que el costo sea real para los modelos WEC<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#hacer-que-el-costo-sea-real-para-los-modelos-wec\" class=\"hash-link\" aria-label=\"Enlace directo al Hacer que el costo sea real para los modelos WEC\" title=\"Enlace directo al Hacer que el costo sea real para los modelos WEC\" translate=\"no\">​</a></h3>\n<p>Langfuse calcula automáticamente el costo para los modelos que conoce (OpenAI, Anthropic), pero la API de Inferencia WEC\nno devuelve costo en dólares y Langfuse no conoce los precios de los modelos de WEC — así que el costo muestra <code>$0</code>. Esta es\nla misma brecha que encontramos en <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/\">parte 1</a>. Soluciona esto una vez:\n<strong>Configuración → Modelos → agregar un modelo</strong> que coincida con <code>Qwen2.5-3B-Instruct</code> con tus precios por token de entrada/salida.\nAhora cada rastreo y el tablero muestran el gasto real — el seguimiento de costos que Promptfoo\nno pudo darnos.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--evaluar-automáticamente-el-tráfico-en-vivo-con-un-juez-llm\">Paso 4 — Evaluar automáticamente el tráfico en vivo con un juez LLM<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#paso-4--evaluar-autom%C3%A1ticamente-el-tr%C3%A1fico-en-vivo-con-un-juez-llm\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — Evaluar automáticamente el tráfico en vivo con un juez LLM\" title=\"Enlace directo al Paso 4 — Evaluar automáticamente el tráfico en vivo con un juez LLM\" translate=\"no\">​</a></h2>\n<p>La evaluación de CI verifica un conjunto fijo. Aquí evaluamos <strong>cada llamada real</strong> automáticamente con un LLM-como-juez.\nEste paso parece pura configuración — también es donde encontramos y solucionamos el mejor error de la\nserie.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"conectar-al-juez\">Conectar al juez<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#conectar-al-juez\" class=\"hash-link\" aria-label=\"Enlace directo al Conectar al juez\" title=\"Enlace directo al Conectar al juez\" translate=\"no\">​</a></h3>\n<p>Crea el evaluador a través de <strong>Evaluadores → + Nuevo evaluador</strong>. Es un asistente de 3 pasos:</p>\n<p><strong>1. Seleccionar Evaluador</strong> — elige la plantilla de <strong>Correctitud</strong> LLM-como-juez.</p>\n<p><strong>2. Configurar conexión LLM</strong> — el juez necesita un modelo para llamar, y aún no hay ninguno, así que haz clic\n<strong>+ Agregar conexión LLM</strong>. El diálogo tiene más campos de los que esperarías — aquí está exactamente lo que necesita cada uno:</p>\n<ul>\n<li class=\"\"><strong>Adaptador LLM:</strong> <code>openai</code></li>\n<li class=\"\"><strong>Nombre del proveedor:</strong> cualquier etiqueta <em>sin dos puntos</em> (por ejemplo, <code>wec-judge</code>) — solo el nombre de la conexión en Langfuse</li>\n<li class=\"\"><strong>Clave API:</strong> tu clave WEC</li>\n<li class=\"\">Haz clic en <strong>Mostrar configuraciones avanzadas</strong>:<!-- -->\n<ul>\n<li class=\"\"><strong>URL base de la API:</strong> <code>https://inference.wiline.com/v1</code> — <strong>crítico</strong>; si se deja en blanco, golpea a OpenAI real</li>\n<li class=\"\"><strong>Usar API de Respuestas:</strong> deja <strong>desactivado</strong> (WEC habla Completions de Chat)</li>\n<li class=\"\"><strong>Encabezados adicionales:</strong> ninguno — omitir</li>\n<li class=\"\"><strong>Habilitar modelos predeterminados:</strong> desactivar <strong>(no quieres la lista de modelos de OpenAI aquí)</strong></li>\n<li class=\"\"><strong>Modelos personalizados → Agregar nombre de modelo personalizado:</strong> <code>Qwen3.5:9B</code> — la elección del modelo importa mucho aquí, como verás</li>\n</ul>\n</li>\n<li class=\"\"><strong>Crear conexión</strong>, luego selecciona el proveedor <strong><code>wec-judge</code></strong> y el modelo <strong><code>Qwen3.5:9B</code></strong> → <strong>Guardar</strong>.</li>\n</ul>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El diálogo de Nueva Conexión LLM — URL base de WEC, modelos predeterminados desactivados, un modelo WEC personalizado agregado\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 4.</strong> El diálogo de Nueva Conexión LLM: adaptador <code>openai</code>, la URL base de la API WEC, modelos\npredeterminados desactivados, y un modelo personalizado agregado — esto es lo que apunta al juez a WEC en lugar de\nOpenAI real.</p>\n<p><strong>3. Ejecutar Evaluador</strong> — agrega un filtro <strong>Nombre = <code>ticket-classify</code></strong>, activa <strong>Ejecutar en observaciones entrantes en vivo</strong> para que evalúe el nuevo tráfico, y <strong>Ejecutar</strong>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La configuración del evaluador LLM-como-juez\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 5.</strong> El evaluador de Correctitud — nombre de la evaluación, el filtro <code>ticket-classify</code>, y \"ejecutar en\nobservaciones entrantes en vivo.\"</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"luego-sin-puntuaciones-depurando-el-tiempo-de-espera-de-120-segundos\">Luego: sin puntuaciones. Depurando el tiempo de espera de 120 segundos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#luego-sin-puntuaciones-depurando-el-tiempo-de-espera-de-120-segundos\" class=\"hash-link\" aria-label=\"Enlace directo al Luego: sin puntuaciones. Depurando el tiempo de espera de 120 segundos\" title=\"Enlace directo al Luego: sin puntuaciones. Depurando el tiempo de espera de 120 segundos\" translate=\"no\">​</a></h3>\n<p>Configuramos todo, enviamos tráfico fresco… y nada. Sin puntuación en ningún rastreo. Así es como lo\nrastreamos — puedes encontrar la misma pared:</p>\n<ol>\n<li class=\"\"><strong>Verifica el registro del evaluador.</strong> <strong>Evaluadores → tu evaluador → Registros</strong> tenía el error real:\n<code>La solicitud ha superado el tiempo de espera después de 120000ms</code>. Cada ejecución, exactamente 120s — así que el juez <em>estaba</em> disparando, su\nllamada LLM simplemente nunca regresó.</li>\n<li class=\"\"><strong>Sospechar del modelo.</strong> Nuestro primer juez, <code>Qwen2.5-3B-Instruct</code>, rechazó las llamadas a herramientas de inmediato\n(400 instantáneo — una configuración vLLM faltante en el backend; más sobre eso a continuación). Cambiamos a\n<code>Qwen3.5:9B</code>, que responde a una llamada directa a la herramienta en ~3s. <strong>Aún así, se agotó el tiempo.</strong></li>\n<li class=\"\"><strong>Sospechar de la red.</strong> <code>curl</code> desde el host: 200 en medio segundo. La misma solicitud de estilo juez desde <em>dentro del contenedor worker</em>: ~3s. Modelo, red, contenedor — todo bien.</li>\n<li class=\"\"><strong>Reproducir lo que Langfuse realmente envía.</strong> El juez no llama a la API como nuestro curl — utiliza <em>salida estructurada</em> (<code>response_format</code>) para que la puntuación regrese en un formato legible por máquina. Reproducir esa forma exacta de llamada expuso el problema: en este backend, <strong>las solicitudes de estilo juez al modelo de razonamiento son increíblemente lentas e impredecibles</strong> — la solicitud idéntica varió de 3 a 95 segundos en diferentes ejecuciones (el modelo \"piensa\" durante mucho tiempo, y el modo JSON añade sobrecarga). Con el prompt real más largo del juez, las ejecuciones superaron el tiempo de espera de 120s. <strong>Nada estaba roto — simplemente era más lento que el tiempo de espera.</strong></li>\n</ol>\n<p>La solución son dos líneas de configuración. Langfuse lee su tiempo de espera LLM de\n<code>LANGFUSE_FETCH_LLM_COMPLETION_TIMEOUT_MS</code> (predeterminado 120000). Dale espacio:</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\"># verifica que se haya aplicado dentro del contenedor:</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>Envía un nuevo rastreo a través de <code>app.py</code>, espera un par de minutos, y:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La puntuación de Correctitud adjunta a un rastreo real\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 6.</strong> Ahí está — <strong>Correctitud: 1.00</strong> adjunta a un rastreo <code>ticket-classify</code> en vivo, con\nel razonamiento del juez a un clic de distancia. Cada nueva llamada ahora se evalúa automáticamente.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Ahora puedes <strong>calificar automáticamente el tráfico real de producción</strong> — y depurar un evaluador que produce\nsilencio: verifica sus Registros, aísla modelo vs. red vs. contenedor, ajusta el tiempo de espera a la realidad.</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>Dos restricciones a tener en cuenta</div><div class=\"admonitionContent_BuS1\"><p><strong>El juez necesita llamadas a herramientas.</strong> Si tu modelo devuelve un 400 en cualquier solicitud de <code>tools</code>, eso\ngeneralmente es la configuración de servicio, no el modelo — en vLLM son las banderas <code>--enable-auto-tool-choice</code> y\n<code>--tool-call-parser</code>. Ese fue nuestro caso: lo informamos, el equipo de la plataforma habilitó las\nbanderas en <code>Qwen2.5-3B-Instruct</code> el mismo día, y el 400 desapareció. Solo la conexión del juez se ve afectada — tu aplicación mantiene su modelo.</p><p><strong>La puntuación es lenta (~2 min/puntuación) y eso es el backend, no tú.</strong> El modelo de razonamiento \"piensa\"\ndurante mucho tiempo en los prompts del juez — vale la pena informarlo a tu equipo de plataforma (límites de presupuesto de razonamiento, o un backend de decodificación guiada como xgrammar en vLLM). El tiempo de espera elevado mantiene la puntuación confiable mientras tanto.</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>Atajo tentador que no funciona: un modelo pequeño como juez</div><div class=\"admonitionContent_BuS1\"><p>Una vez que se habilitó la llamada a herramientas en <code>Qwen2.5-3B</code>, lo intentamos como juez — las puntuaciones regresaron en\n<strong>2–10 segundos</strong> en lugar de ~2 minutos. Pero eran basura: el mismo tipo de respuesta correcta obtuvo\n<strong>1, 1, 0 y 0.05</strong> en cuatro ejecuciones, con racionales casi idénticos adjuntos a puntuaciones opuestas. El modelo pequeño llena el formato de puntuación perfectamente; simplemente no puede <em>evaluar</em> realmente.\nImagen espejo del hallazgo de <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/\">parte 3</a>: el modelo pequeño gana en <em>clasificación</em> y pierde en <em>evaluación</em> — la evaluación es una tarea de razonamiento. Mantén el juez en <code>Qwen3.5:9B</code>: lento pero coherente.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5--encontrar-el-fallo-que-tus-pruebas-pasaron-por-alto\">Paso 5 — Encontrar el fallo que tus pruebas pasaron por alto<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#paso-5--encontrar-el-fallo-que-tus-pruebas-pasaron-por-alto\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 5 — Encontrar el fallo que tus pruebas pasaron por alto\" title=\"Enlace directo al Paso 5 — Encontrar el fallo que tus pruebas pasaron por alto\" translate=\"no\">​</a></h2>\n<p>Aquí es donde el <strong>árbol de spans</strong> da sus frutos. Abre un rastreo <code>handle_ticket</code> y no obtienes un solo blob opaco — obtienes la <strong>cadena completa como un árbol</strong>, cada paso con su propia entrada, salida, latencia y\ntokens:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Profundizando en un rastreo — el árbol de spans localiza cada paso\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 7.</strong> El árbol <code>handle_ticket</code> — <code>handle_ticket → ticket-classify → draft-reply</code> — con\nlatencia y tokens por paso. Cada paso tiene una dirección que puedes inspeccionar.</p>\n<p>Esa estructura es lo que hace que los fallos de producción sean depurables. Supongamos que un cliente recibe una respuesta sin sentido.\nSin rastreo solo sabrías \"la respuesta fue mala.\" Con el árbol lo expandes y lees cada paso — por ejemplo:</p>\n<ul>\n<li class=\"\"><strong><code>ticket-classify</code></strong> → <code>categoría: \"facturación\"</code> ✅ correcto — así que el clasificador no es el problema.</li>\n<li class=\"\"><strong><code>draft-reply</code></strong> → inventó una política de reembolso que no existe ❌ — <strong>el error está en el paso 2.</strong></li>\n</ul>\n<p>Has localizado el fallo en el <strong>paso exacto, entrada y salida</strong> — deberías corregir el prompt de <code>draft-reply</code>,\nno perder tiempo en el clasificador. La evaluación fuera de línea te dice que la tasa de aprobación cayó; el rastreo\nte dice <em>qué llamada, en qué entrada, produjo qué salida incorrecta, en qué paso.</em> Y ahora que el Paso 4 evalúa cada llamada, ni siquiera tienes que buscar: <strong>filtra Trazado por puntuación baja</strong> y los rastros sospechosos emergen por sí mismos — a menudo una redacción que tu conjunto de datos de la parte 3 nunca cubrió, que es exactamente lo que capturas de nuevo a continuación.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Ahora puedes <strong>asignar un fallo de producción al paso exacto, entrada y salida</strong> — no más adivinanzas\nsobre qué llamada inventó esa política de reembolso.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-6--cerrar-el-ciclo-producción--mejores-evaluaciones\">Paso 6 — Cerrar el ciclo: producción → mejores evaluaciones<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#paso-6--cerrar-el-ciclo-producci%C3%B3n--mejores-evaluaciones\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 6 — Cerrar el ciclo: producción → mejores evaluaciones\" title=\"Enlace directo al Paso 6 — Cerrar el ciclo: producción → mejores evaluaciones\" translate=\"no\">​</a></h2>\n<p>Un fallo de producción es un <strong>caso de prueba faltante</strong> — y Langfuse hace que capturarlo sea un clic. Crea\nel conjunto de datos una vez (<strong>Conjuntos de datos → + Nuevo conjunto de datos</strong>, por ejemplo, <code>fallos-de-producción</code>), luego en cualquier rastreo haz clic en\n<strong>+ Agregar a conjuntos de datos</strong> y selecciónalo. La entrada y salida del rastreo se completan automáticamente como un nuevo ítem:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Agregando un rastreo de nuevo al conjunto de datos de evaluación\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 8.</strong> \"Agregar a conjuntos de datos\" convierte un rastreo de producción en un nuevo caso de prueba — entrada y salida esperada completadas automáticamente — alimentándolo directamente de nuevo a tu conjunto de datos de la parte 3.</p>\n<p>Para automatizarlo, extrae los rastros que has marcado o agregado a un conjunto de datos a través del SDK de Langfuse, escribe\nfilas <code>{ticket, categoría, prioridad}</code> en <code>tickets_dataset.jsonl</code>, y vuelve a ejecutar la evaluación de Promptfoo de la parte 1/parte 3 — ahora cubre el caso del mundo real.</p>\n<p>Ahora tu evaluación de CI es más fuerte <em>porque</em> la producción encontró un agujero en ella. Ese es el ciclo virtuoso:\n<strong>evaluación fuera de línea → enviar → observar y evaluar tráfico real → capturar fallos → la evaluación fuera de línea se vuelve\nmás fuerte.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Has cerrado el ciclo: <strong>los fallos de producción ahora hacen que tus evaluaciones sean más fuertes</strong> — el ciclo virtuoso que\nmantiene una función de IA buena después de que se envía.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-ahora-puedes-hacer\">Lo que ahora puedes hacer<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#lo-que-ahora-puedes-hacer\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que ahora puedes hacer\" title=\"Enlace directo al Lo que ahora puedes hacer\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong>Ver dentro de</strong> cada llamada WEC en producción (entrada/salida/latencia/costo).</li>\n<li class=\"\"><strong>Evaluar tráfico real automáticamente</strong> con un juez LLM — y depurar el evaluador mismo cuando se queda en silencio.</li>\n<li class=\"\"><strong>Depurar un fallo específico</strong> hasta el paso exacto, no solo una métrica agregada.</li>\n<li class=\"\"><strong>Convertir fallos de producción en casos de prueba</strong>, para que tus evaluaciones sigan mejorando.</li>\n</ul>\n<p>Esa es la etapa de \"mantenerlo bueno después del envío\". La evaluación fuera de línea (partes 1–3) prueba que funciona antes\ndel lanzamiento; esto prueba — y mantiene — que funciona después.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-de-problemas\">Solución de problemas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#soluci%C3%B3n-de-problemas\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas\" title=\"Enlace directo al Solución de problemas\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"dirección-ya-en-uso-en-docker-compose-up\"><a id=\"ts-ports\"></a>\"dirección ya en uso\" en <code>docker compose up</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#direcci%C3%B3n-ya-en-uso-en-docker-compose-up\" class=\"hash-link\" aria-label=\"Enlace directo al dirección-ya-en-uso-en-docker-compose-up\" title=\"Enlace directo al dirección-ya-en-uso-en-docker-compose-up\" translate=\"no\">​</a></h3>\n<p>Otro servicio ocupa el puerto 3000 (web) o 5432 (Postgres). Las bases de datos no necesitan puertos de host —\nen <code>docker-compose.yml</code>, remapea <code>langfuse-web</code> a un puerto libre (<code>3001:3000</code>) y elimina el mapeo del host de Postgres. Compose <em>fusiona</em> las listas de <code>ports</code>, así que un archivo de anulación con <code>ports: []</code> no las eliminará; edita el compose directamente.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-contenedor-web-no-arranca\">El contenedor web no arranca<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#el-contenedor-web-no-arranca\" class=\"hash-link\" aria-label=\"Enlace directo al El contenedor web no arranca\" title=\"Enlace directo al El contenedor web no arranca\" translate=\"no\">​</a></h3>\n<p>Casi siempre <code>ENCRYPTION_KEY</code> — debe ser <code>openssl rand -hex 32</code> (64 caracteres hexadecimales), no base64.\nVerifica <code>docker compose logs langfuse-web</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"el-costo-muestra-0\">El costo muestra <code>$0</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#el-costo-muestra-0\" class=\"hash-link\" aria-label=\"Enlace directo al el-costo-muestra-0\" title=\"Enlace directo al el-costo-muestra-0\" translate=\"no\">​</a></h3>\n<p>Langfuse no tiene precio para los modelos de WEC por defecto. Agrega el modelo + precios por token en\n<strong>Configuración → Modelos</strong> (Paso 3).</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"no-aparecen-puntuaciones-en-nuevos-rastreos\">No aparecen puntuaciones en nuevos rastreos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#no-aparecen-puntuaciones-en-nuevos-rastreos\" class=\"hash-link\" aria-label=\"Enlace directo al No aparecen puntuaciones en nuevos rastreos\" title=\"Enlace directo al No aparecen puntuaciones en nuevos rastreos\" translate=\"no\">​</a></h3>\n<p>Verifica <strong>Evaluadores → tu evaluador → Registros</strong> primero — un error allí te dice que está ejecutándose y\nfallando, no inactivo. Luego empareja el error:</p>\n<ul>\n<li class=\"\"><strong>Un 400 mencionando llamadas a herramientas / <code>tool_choice</code></strong> — la llamada a herramientas no está habilitada para ese modelo en\nel backend de servicio (en vLLM: las banderas <code>--enable-auto-tool-choice</code> / <code>--tool-call-parser</code>).\nDos formas de salir: apunta la Conexión LLM del juez a un modelo donde funcione (<code>Qwen3.5:9B</code>), o pide\na tu equipo de plataforma que habilite las banderas — eso es lo que solucionó <code>Qwen2.5-3B-Instruct</code> para nosotros. De cualquier manera, solo cambia el juez — tu aplicación mantiene su modelo.</li>\n<li class=\"\"><strong><code>La solicitud ha superado el tiempo de espera después de 120000ms</code></strong> — la llamada de salida estructurada del juez es más lenta que\nel tiempo de espera predeterminado de Langfuse. Aumenta <code>LANGFUSE_FETCH_LLM_COMPLETION_TIMEOUT_MS</code> (Paso 4) y\nconfirma con <code>docker compose exec langfuse-worker env | grep TIMEOUT</code>. Si <code>curl</code> simple es rápido\npero la puntuación aún se agota, no persigas la red — la ruta de salida estructurada es lo que es\nlento, y eso es una solución de backend.</li>\n</ul>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"las-puntuaciones-tardan-2-minutos-cada-una\">Las puntuaciones tardan ~2 minutos cada una<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#las-puntuaciones-tardan-2-minutos-cada-una\" class=\"hash-link\" aria-label=\"Enlace directo al Las puntuaciones tardan ~2 minutos cada una\" title=\"Enlace directo al Las puntuaciones tardan ~2 minutos cada una\" translate=\"no\">​</a></h3>\n<p>Eso no es un colapso — es el modelo de razonamiento generando una larga cadena de pensamiento oculta en los prompts del juez (muy variable: la misma solicitud puede tardar 3s o 95s), además de la sobrecarga de salida estructurada.\nAumentar el tiempo de espera hace que la puntuación sea <em>confiable</em> pero no <em>rápida</em> — la verdadera solución está del lado de la plataforma\n(límites de presupuesto de razonamiento, backend de decodificación guiada).</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=\"qué-sigue\">Qué sigue<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al Qué sigue\" title=\"Enlace directo al Qué sigue\" translate=\"no\">​</a></h2>\n<p>Ahora puedes construir, probar, observar y mejorar una sola función WEC. La última etapa es el patrón real\nde la aplicación: <strong>RAG</strong> — recuperación + generación — con todo de las partes 1–4 integrado\n(salida verificada por esquema, un conjunto de datos de evaluación, y rastreo de Langfuse sobre todo el pipeline). Ahí es donde todo se une.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lectura-adicional\">Lectura adicional<a href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/#lectura-adicional\" class=\"hash-link\" aria-label=\"Enlace directo al Lectura adicional\" title=\"Enlace directo al Lectura adicional\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://langfuse.com/self-hosting\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Langfuse — documentación de autoalojamiento</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 — compensaciones</a></li>\n<li class=\"\"><a href=\"https://posthog.com/blog/best-open-source-llm-observability-tools\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Herramientas de observabilidad LLM de código abierto comparadas (PostHog)</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/",
            "title": "Captura lo que tus pruebas no ven: observa y evalúa tu aplicación WEC en producción con Langfuse",
            "summary": "Las evaluaciones de CI pasan en un conjunto de pruebas fijo, pero la producción envía entradas que nunca probaste. Autoalojar Langfuse en una instancia WEC, rastrear cada llamada real, evaluar automáticamente el tráfico en vivo con un juez LLM, profundizar en el paso exacto que falló y retroalimentar los fallos para hacer tus evaluaciones más fuertes. Cada comando — y cada callejón sin salida — es real.",
            "date_modified": "2026-07-07T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "observabilidad",
                "langfuse",
                "evals",
                "inferencia",
                "producción",
                "autoalojamiento"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/promptfoo-avatar-31b32c657d72dac0a7edbd77c24ec717.jpg\" alt=\"Promptfoo\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"data:image/png;base64,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\" alt=\"JSON\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 8 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->8</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->8<!-- --> earned</span></div><div class=\"skillTracker__series\">AI evals &amp; observability</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove a model works</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trustworthy JSON</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">3</span><span class=\"skillTracker__skill\" data-state=\"current\">Real test data at scale</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Observe &amp; score production</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">RAG, end to end</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">Catch regressions in CI</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trace &amp; debug agent tool calls</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/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>Tu evaluación pasa el 100% — de los <strong>dos casos de prueba que escribiste a mano.</strong> Los usuarios reales no expresarán las cosas como tú lo hiciste.</p>\n<p>En <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/\">parte 1</a> y\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/\">parte 2</a> construimos un arnés de evaluación real —\nafirmaciones, validación de esquema, una matriz de modelos. Pero dos casos no pueden decirte si tu aplicación funciona;\nsolo pueden decirte que no se bloqueó con dos entradas.</p>\n<p>Esta guía soluciona eso. La habilidad es <strong>producir un conjunto de datos en el que realmente puedas confiar</strong>: usamos la\n<strong>API de Inferencia de WEC</strong> para <strong>generar tickets etiquetados</strong>, luego <strong>validarlos y curarlos</strong> — porque\nlas etiquetas generadas <em>no</em> son automáticamente correctas, y tratarlas como verdad solo mueve el error.\nEl resultado es datos de prueba reales a gran escala. Cuando finalmente lo ejecutes a través del arnés de la parte 2, la cobertura\nrevela las fallas — malas clasificaciones genuinas <em>y</em> etiquetas debatibles — que dos casos seleccionados a mano ocultan.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"cómo-se-integra-todo\">Cómo se integra todo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/#c%C3%B3mo-se-integra-todo\" class=\"hash-link\" aria-label=\"Enlace directo al Cómo se integra todo\" title=\"Enlace directo al Cómo se integra todo\" translate=\"no\">​</a></h2>\n<p>Cada paso alimenta al siguiente, terminando en un conjunto de datos que se conecta directamente a la evaluación de la parte 2:</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">\n<p>Una <strong>clave de API de Inferencia de WEC</strong> — consulta <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/cloud_portal/platform/inference/api_keys/\">Inferencia → Claves de API</a>.</p>\n</li>\n<li class=\"\">\n<p><strong><code>jq</code></strong>, <strong><code>curl</code></strong>, y <strong>Node.js 22+</strong> (Promptfoo se ejecuta a través de <code>npx</code>).</p>\n</li>\n<li class=\"\">\n<p>Tu clave exportada y saneada (una clave pegada a menudo lleva caracteres invisibles):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash 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>Si una llamada devuelve un <code>401</code> / \"clave de API inválida\", tu clave ha expirado o ha sido revocada — genera una\nnueva en <strong>Inferencia → Claves de API</strong>. Consulta <a href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/#ts-401\" class=\"\">Solución de problemas</a>.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--generar-un-lote-etiquetado\">Paso 1 — Generar un lote etiquetado<a href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/#paso-1--generar-un-lote-etiquetado\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Generar un lote etiquetado\" title=\"Enlace directo al Paso 1 — Generar un lote etiquetado\" translate=\"no\">​</a></h2>\n<p>Pide al modelo tickets <strong>con sus respuestas</strong> — las etiquetas son lo que lo convierte en un <em>conjunto de datos</em>, no\nsolo entradas. Reutilizamos el esquema exacto de la parte 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\":\"Genera 8 tickets de soporte al cliente diversos y realistas para una empresa de alojamiento en la nube. Devuelve SOLO JSONL (un objeto JSON por línea), cada uno con claves: ticket (cadena), category (uno de: billing, technical, account, other), priority (uno de: low, medium, high). Sin markdown, sin cercas.\"}]</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\">Salida</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\":\"Nuestro servidor se cayó inesperadamente y no estamos seguros de qué lo causó.\",\"category\":\"technical\",\"priority\":\"high\"}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">{\"ticket\":\"Necesitamos actualizar nuestro plan pero el sistema muestra un mensaje de error al intentar cambiar de planes.\",\"category\":\"account\",\"priority\":\"medium\"}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">{\"ticket\":\"Estamos teniendo problemas para acceder a nuestra base de datos y no podemos iniciar sesión.\",\"category\":\"technical\",\"priority\":\"high\"}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">... 5 líneas más ...</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Paso 1 — una llamada devuelve JSONL limpio y etiquetado\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 1.</strong> Una única generación: 8 tickets diversos, cada uno ya llevando su etiqueta de <code>category</code> y <code>priority</code>.</p>\n<p>JSONL limpio, etiquetas válidas, verdadera diversidad — y regresó en segundos. Elegimos deliberadamente un\n<strong>modelo pequeño y no razonador</strong> (<code>Qwen2.5-3B-Instruct</code>) para la generación. Aquí está el porqué de su importancia.</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>¿Por qué no un modelo razonador para generación masiva?</div><div class=\"admonitionContent_BuS1\"><p>Los modelos razonadores devuelven su cadena de pensamiento en un campo separado <strong><code>reasoning_content</code></strong> — tokens\npor los que pagas pero que nunca usas. Pide a un modelo razonador como <code>Qwen3.5:9B</code> algunos tickets y compara el\nrazonamiento oculto con la respuesta real:</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\":\"Genera 3 tickets de soporte al cliente realistas para una empresa de alojamiento en la nube. Devuelve SOLO JSONL, claves: ticket, category, priority. Sin markdown, sin cercas.\"}]}'</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\">Salida</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=\"La salida de un modelo razonador — el razonamiento oculto eclipsa la respuesta\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 1b.</strong> Para solo 3 tickets, <code>Qwen3.5:9B</code> escribió <strong>7,116 caracteres de razonamiento oculto para\nproducir 412 de respuesta</strong> — pagas por todos los ~2,000 tokens de finalización. (Los conteos varían por llamada; la\nasimetría no.) Por eso generamos con el pequeño y no razonador <code>Qwen2.5-3B</code> — y es la misma razón por la que esos modelos razonadores tropiezan con la salida estructurada en la matriz de la Parte 5.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--valida-antes-de-confiar-en-ello\">Paso 2 — Valida antes de confiar en ello<a href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/#paso-2--valida-antes-de-confiar-en-ello\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — Valida antes de confiar en ello\" title=\"Enlace directo al Paso 2 — Valida antes de confiar en ello\" translate=\"no\">​</a></h2>\n<p>Nunca trates los datos generados como verdad absoluta sin verificarlos. Guarda un lote y valida tres\ncosas: cada línea se analiza, cada etiqueta está en el enum, y las clases están razonablemente equilibradas.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\":\"Genera 8 tickets de soporte al cliente diversos y realistas para una empresa de alojamiento en la nube. Devuelve SOLO JSONL (un objeto JSON por línea), cada uno con claves: ticket (cadena), category (uno de: billing, technical, account, other), priority (uno de: low, medium, high). Sin markdown, sin cercas.\"}]}'</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) ¿todas las líneas se analizan como 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\">\"todas las líneas son JSON válidos\"</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\">\"JSON INVÁLIDO presente\"</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) ¿hay etiquetas fuera del enum? (sin filas impresas = todas válidas)</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) distribución de clases</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\">Salida</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\">todas las líneas son JSON válidos</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=\"Paso 2 — validando el lote: JSON válidos, etiquetas en el enum, distribución de clases\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 2.</strong> Salida de validación — cada línea se analiza, sin etiquetas fuera del enum, y una distribución de clases razonable.</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>El intuitivo <code>[\"billing\",...] | index(.category)</code> <strong>falla</strong> con <em>\"No se puede indexar un array con\ncadena\"</em> — dentro del pipe, <code>.</code> es el array, así que <code>.category</code> intenta indexarlo. Usa\n<strong><code>.category | IN(\"billing\", ...)</code></strong> en su lugar. Consulta <a href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/#ts-jq\" class=\"\">Solución de problemas</a>.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--escalar-y-eliminar-duplicados\">Paso 3 — Escalar y eliminar duplicados<a href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/#paso-3--escalar-y-eliminar-duplicados\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — Escalar y eliminar duplicados\" title=\"Enlace directo al Paso 3 — Escalar y eliminar duplicados\" translate=\"no\">​</a></h2>\n<p>Un lote de 8 no es suficiente. Repite la llamada varias veces (temperatura más alta para variedad entre\nlotes), concatena, y luego elimina duplicados por texto del 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 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\":\"Genera 8 tickets de soporte al cliente diversos y realistas para una empresa de alojamiento en la nube. Devuelve SOLO JSONL (un objeto JSON por línea), cada uno con claves: ticket (cadena), category (uno de: billing, technical, account, other), priority (uno de: low, medium, high). Sin markdown, sin cercas.\"}]}'</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\">\"lote </span><span class=\"token string variable\" style=\"color:#36acaa\">$i</span><span class=\"token string\" style=\"color:#e3116c\"> hecho\"</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\"># eliminar duplicados por texto del ticket</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\">\"crudo: </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\">  →  deduplicado: </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\">Salida</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\">crudo: 40  →  deduplicado: 40</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Paso 3 — cinco lotes concatenados y deduplicados a 40 filas únicas\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 3.</strong> Escalando: 5 lotes → 40 filas después de la eliminación de duplicados exactos.</p>\n<p>Cero duplicados exactos en 5 lotes — una buena señal de que el modelo no se está repitiendo.</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>precaución</div><div class=\"admonitionContent_BuS1\"><p><code>unique_by(.ticket)</code> solo captura texto <strong>idéntico</strong>. Dos tickets \"el servidor está caído\" redactados de manera diferente son duplicados <em>semánticos</em> y se deslizarán — capturarlos requiere agrupamiento basado en incrustaciones, un paso más avanzado. Para un conjunto de datos inicial, la deduplicación exacta está bien.</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>Protégete contra fallas silenciosas</div><div class=\"admonitionContent_BuS1\"><p>Nota el <strong><code>// empty</code></strong> en el filtro <code>jq</code>. Si una llamada falla (clave revocada, tiempo de espera del modelo), la\nrespuesta no tiene contenido y <code>.choices[0].message.content</code> es <code>null</code> — un simple <code>jq -r</code> escribiría la palabra literal <code>null</code> en tu conjunto de datos, envenenándolo silenciosamente. <code>// empty</code> elimina esos,\nasí que una llamada fallida agrega <strong>nada</strong> en lugar de una fila basura. Siempre verifica el conteo de filas después.\nConsulta <a href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/#ts-null\" class=\"\">Solución de problemas</a>.</p></div></div>\n<p>Valida las 40 completas de la misma manera que en el Paso 2 (<code>jq -e .</code>, la verificación del enum, y ambas distribuciones).\nLas nuestras resultaron equilibradas: técnico 15 · cuenta 10 · facturación 10 · otro 5; prioridad alta 13 ·\nmedia 14 · baja 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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Ahora tienes un <strong>pipeline repetible para datos de prueba reales</strong> — 40 filas etiquetadas, validadas y deduplicadas\ndonde antes tenías dos.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--convertir-a-un-conjunto-de-datos-de-promptfoo\">Paso 4 — Convertir a un conjunto de datos de Promptfoo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/#paso-4--convertir-a-un-conjunto-de-datos-de-promptfoo\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — Convertir a un conjunto de datos de Promptfoo\" title=\"Enlace directo al Paso 4 — Convertir a un conjunto de datos de Promptfoo\" translate=\"no\">​</a></h2>\n<p>Promptfoo lee casos de prueba desde un CSV: cada columna se convierte en una variable, y la columna especial\n<strong><code>__expected</code></strong> contiene una afirmación por fila. Mapeamos <code>ticket</code> → la variable de prompt y\n<code>__expected</code> → <code>icontains:&lt;la categoría correcta&gt;</code>. <code>jq @csv</code> maneja las comas y comillas\ndentro del texto del 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 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 = encabezado + 40 filas</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5--poner-el-conjunto-de-datos-a-trabajar\">Paso 5 — Poner el conjunto de datos a trabajar<a href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/#paso-5--poner-el-conjunto-de-datos-a-trabajar\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 5 — Poner el conjunto de datos a trabajar\" title=\"Enlace directo al Paso 5 — Poner el conjunto de datos a trabajar\" translate=\"no\">​</a></h2>\n<p>El conjunto de datos es el entregable — todo desde aquí es lo que <em>desbloquea</em>. Primer beneficio: una comparación de modelos\nen la que realmente puedes confiar. En dos casos escritos a mano, un ranking es ruido; en 40 filas reales,\netiquetadas es una medición. Agrega cada modelo WEC como proveedor y ejecuta todo el conjunto:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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: \"Clasificador de tickets — matriz de modelos sobre el conjunto de datos sintético de 40 filas\"</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\">    Clasifica este ticket de soporte. Devuelve SOLO JSON con claves:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"category\" (uno de: billing, technical, account, other),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"priority\" (uno de: low, medium, high),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"summary\" (cadena, máximo 12 palabras).</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>El resultado es lo opuesto a lo que \"más grande es mejor\" predeciría:</p>\n<table><thead><tr><th>Modelo</th><th>Precisión</th><th>Tokens de finalización / ticket</th></tr></thead><tbody><tr><td><strong><code>Qwen2.5-3B-Instruct</code></strong> (no razonador)</td><td><strong>85%</strong> (34/40)</td><td>~30</td></tr><tr><td><code>Qwen3.5:9B</code> (razonador)</td><td>47.5% (19/40)</td><td>~965</td></tr><tr><td><code>Qwen3.5-122B</code> (razonador)</td><td>15% (6/40)</td><td>~1,013</td></tr></tbody></table>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Paso 5 — la matriz de modelos sobre el conjunto de datos de 40 filas\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 4.</strong> Mismo conjunto de datos, tres modelos — el pequeño modelo no razonador gana por mucho.</p>\n<p><strong>El pequeño modelo no razonador gana decisivamente — y cuanto más grande es el modelo razonador, <em>peor</em>\nlo hace.</strong> Los modelos razonadores no son más tontos al <em>clasificar</em>; <strong>enterraron el JSON en su\ncadena de pensamiento</strong>. <code>Qwen3.5:9B</code> filtra <code>Thinking: …</code> en la salida; <code>Qwen3.5-122B</code> a menudo devuelve\nuna <strong>respuesta vacía por completo</strong> — todos los ~1,000 tokens gastados razonando, nada parseable queda. También\nqueman <strong>~30× más tokens por ticket</strong>, razón por la cual la matriz completa tomó <strong>12m 31s</strong>, casi todo\nel tiempo esperando a los dos grandes modelos. <strong>Para salida estructurada, pensar en voz alta es una desventaja,\nno una ventaja</strong> — un hallazgo contraintuitivo que nunca obtendrías de una tabla de clasificación.</p>\n<p>Pero nota lo que realmente hizo posible ese hallazgo: <strong>el conjunto de datos.</strong> Con dos casos escritos a mano esos\nporcentajes serían lanzamientos de moneda; 40 filas etiquetadas son lo que convierte \"¿qué modelo?\" de una suposición en una\nmedición. La comparación es la <em>recompensa</em> por construir los datos — no un sustituto de ellos.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Ahora puedes <strong>elegir un modelo con evidencia en lugar de sensaciones</strong> — algo que ninguna tabla de clasificación puede hacer por\nti, porque no conoce tu tarea.</p></div></div>\n<p>(El <code>transform</code> captura el primer bloque <code>{…}</code> con una coincidencia <strong>no codiciosa</strong> <code>*?</code> — un seguro barato para\nmodelos que envuelven JSON en prosa, exactamente como en <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/\">parte 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>Evaluamos <code>category</code> aquí — <code>priority</code> es tu próxima afirmación</div><div class=\"admonitionContent_BuS1\"><p>La columna <code>__expected</code> califica solo la <strong>categoría</strong> predicha contra la etiqueta. El conjunto de datos también\nlleva una etiqueta de <code>priority</code> que aún no calificamos. Para calificar ambos campos a la vez, agrega una\nafirmación de <code>javascript</code> que analice la salida JSON y compare también <code>priority</code> — el mismo patrón exacto, una verificación más.\nCalificar más de lo que generaste es la forma más barata de hacer una evaluación más estricta.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-6--profundiza-en-las-fallas-del-ganador\">Paso 6 — Profundiza en las fallas del ganador<a href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/#paso-6--profundiza-en-las-fallas-del-ganador\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 6 — Profundiza en las fallas del ganador\" title=\"Enlace directo al Paso 6 — Profundiza en las fallas del ganador\" translate=\"no\">​</a></h2>\n<p>Toma el ganador — <code>Qwen2.5-3B</code> con <strong>85% (34/40)</strong> — y extrae sus 6 tickets fallidos, comparando cada uno\ncon su etiqueta. (<code>__expected</code> no se almacena como una variable — Promptfoo lo consume como la afirmación —\nasí que lee la verdad de la fuente de datos). Debido a que los datos son recién generados, tus fallas exactas\ndiferirán, pero se dividen consistentemente en dos tipos reconocibles — y <strong>ninguno es solo \"el modelo es tonto\". Ejemplos representativos:</strong></p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El informe de Promptfoo — paso/falla por fila en el conjunto de datos de 40 casos\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 5.</strong> El informe web facilita escanear qué filas fallaron y abrir cada una.</p>\n<table><thead><tr><th>Ticket</th><th>Etiqueta</th><th>Por qué es complicado</th></tr></thead><tbody><tr><td>\"descuentos educativos para startups…\"</td><td><code>other</code></td><td><strong>Debatible</strong> — se trata de precios, así que <code>billing</code> es defensible</td></tr><tr><td>\"otorgar acceso IAM a S3 a un desarrollador junior…\"</td><td><code>account</code></td><td><strong>Debatible</strong> — control de acceso vs infraestructura</td></tr><tr><td>\"comentarios sobre la documentación de la API…\"</td><td><code>other</code></td><td><strong>Debatible</strong> — comentarios vs <code>technical</code></td></tr><tr><td>\"actualizar la tarjeta <strong>y</strong> agregar un usuario…\"</td><td><code>account</code></td><td><strong>Multi-intención</strong> — genuinamente dos categorías a la vez</td></tr></tbody></table>\n<p>El resto son malas clasificaciones ordinarias. Las lecciones reales, del día a día:</p>\n<ul>\n<li class=\"\"><strong>Tus etiquetas sintéticas no son verdad absoluta.</strong> Un bloque de \"fallas\" son disputas de etiquetas — revísalas y\ncorrígelas, o tu evaluación mide lo incorrecto.</li>\n<li class=\"\"><strong>La clasificación de una sola etiqueta falla en tickets de múltiples intenciones.</strong> Las entradas reales no siempre son de una categoría.</li>\n<li class=\"\"><strong>Un 85% de encabezado oculta ambas.</strong> Solo aprendes <em>por qué</em> leyendo las filas que fallaron — que es la\nrazón completa para evaluar en un conjunto de datos real en lugar de en dos ejemplos.</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-construimos\">Lo que construimos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/#lo-que-construimos\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que construimos\" title=\"Enlace directo al Lo que construimos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">La habilidad central: un pipeline repetible para <strong>generar, validar, deduplicar y versionar</strong> un conjunto de datos de evaluación\ndesde la API de WEC — datos de prueba reales en lugar de dos casos escritos a mano.</li>\n<li class=\"\">La disciplina que lo hace confiable: <strong>las etiquetas generadas no son verdad absoluta.</strong> Validas enums\ny equilibrio, y <em>revisas las disputas</em> — un bloque de \"fallas\" son tus propias etiquetas siendo incorrectas.</li>\n<li class=\"\">Prueba de que importa: <strong>la cobertura revela lo que dos casos ocultan</strong> — malas clasificaciones genuinas <em>y</em>\ntickets debatibles y de múltiples intenciones que nunca pensarías en escribir a mano.</li>\n<li class=\"\">Un bono que los datos desbloquean: una <strong>comparación de modelos confiable</strong> — aquí un <strong>modelo no razonador de 3B\nsuperó a uno razonador de 122B</strong> para salida estructurada, lo opuesto a \"más grande es mejor.\"</li>\n</ul>\n<p>Compromete <code>tickets_dataset.jsonl</code> y <code>tests.csv</code> junto a tu configuración de evaluación, y regenera/crece\nel conjunto de datos a medida que tu producto cambia.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-de-problemas\">Solución de problemas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/#soluci%C3%B3n-de-problemas\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas\" title=\"Enlace directo al Solución de problemas\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"401--clave-de-api-inválida\"><a id=\"ts-401\"></a>401 / clave de API inválida<a href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/#401--clave-de-api-inv%C3%A1lida\" class=\"hash-link\" aria-label=\"Enlace directo al 401--clave-de-api-inválida\" title=\"Enlace directo al 401--clave-de-api-inválida\" 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\">Salida</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\": \"Error de autenticación, clave de API inválida\", \"code\": \"401\" } }</span><br></div></code></pre></div></div>\n<p>La clave ha expirado, ha sido revocada o está mal formada. Genera una nueva en <strong>Inferencia → Claves de API</strong>,\nreexporta y vuelve a sanear (<code>tr -cd '[:print:]'</code>) en caso de que el pegado haya llevado un carácter invisible.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"jq-no-se-puede-indexar-un-array-con-cadena\"><a id=\"ts-jq\"></a>jq: \"No se puede indexar un array con cadena\"<a href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/#jq-no-se-puede-indexar-un-array-con-cadena\" class=\"hash-link\" aria-label=\"Enlace directo al jq-no-se-puede-indexar-un-array-con-cadena\" title=\"Enlace directo al jq-no-se-puede-indexar-un-array-con-cadena\" translate=\"no\">​</a></h3>\n<p>Esto proviene de <code>[\"billing\", ...] | index(.category)</code> — dentro del pipe, <code>.</code> es el array, así que\n<code>.category</code> indexa el array. Usa el idiom <code>IN()</code> en su lugar:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_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=\"El error &amp;quot;No se puede indexar un array con cadena&amp;quot; de jq\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 6.</strong> El error de <code>index(.field)</code> — dentro del pipe, jq intenta indexar el array con una cadena.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"tu-conjunto-de-datos-tiene-filas-que-son-solo-null\"><a id=\"ts-null\"></a>Tu conjunto de datos tiene filas que son solo <code>null</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/#tu-conjunto-de-datos-tiene-filas-que-son-solo-null\" class=\"hash-link\" aria-label=\"Enlace directo al tu-conjunto-de-datos-tiene-filas-que-son-solo-null\" title=\"Enlace directo al tu-conjunto-de-datos-tiene-filas-que-son-solo-null\" translate=\"no\">​</a></h3>\n<p>Una fila <code>null</code> significa que una llamada de generación falló pero el bucle aún escribió su resultado (vacío). Causas habituales: una clave de API expirada/revocada, o el modelo que se agota. Soluciones: extraer con\n<code>.choices[0].message.content // empty</code> para que una llamada fallida escriba <strong>nada</strong> en lugar de <code>null</code>;\nvuelve a verificar <code>wc -l</code> después de cada ejecución de generación; y si las llamadas se cuelgan, agrega un tiempo de espera por solicitud\n(<code>curl -m 60</code>) y considera un modelo más ligero y rápido para la generación masiva.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"__expected-es-null-en-el-json-de-resultados\"><code>__expected</code> es <code>null</code> en el JSON de resultados<a href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/#__expected-es-null-en-el-json-de-resultados\" class=\"hash-link\" aria-label=\"Enlace directo al __expected-es-null-en-el-json-de-resultados\" title=\"Enlace directo al __expected-es-null-en-el-json-de-resultados\" translate=\"no\">​</a></h3>\n<p>Eso es esperado — Promptfoo <strong>consume</strong> la columna <code>__expected</code> del CSV como la afirmación de la fila, así que\nno se mantiene como una variable. Para inspeccionar las etiquetas de verdad para las filas que fallan, léelas de tu\nconjunto de datos fuente (<code>tickets_dataset.jsonl</code>) en lugar del archivo de resultados.</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=\"qué-sigue\">¿Qué sigue?<a href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al ¿Qué sigue?\" title=\"Enlace directo al ¿Qué sigue?\" translate=\"no\">​</a></h2>\n<p>Ahora tienes un conjunto de datos real — pero se ejecuta en CI, fuera de línea. El siguiente paso es observar tu aplicación en\n<strong>producción</strong>: autoalojar <strong>Langfuse</strong> para rastrear cada llamada, seguir la latencia y el costo, y ejecutar\nevaluaciones (usando <em>este conjunto de datos</em>) contra el tráfico en vivo. Ahí es donde las pruebas fuera de línea se convierten en\nuna verdadera observabilidad — y es la próxima publicación de la serie.</p>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/",
            "title": "Deja de escribir casos de prueba a mano: genera un conjunto de evaluación con la API de WEC",
            "summary": "Dos casos de prueba escritos a mano no son una evaluación. Usa la API de Inferencia de WEC para generar un conjunto de datos de evaluación etiquetado — luego valídalo y curátalo, porque las etiquetas generadas no son automáticamente correctas. El resultado es datos de prueba reales a gran escala; aliméntalo a tu arnés y la cobertura revela las malas clasificaciones y las etiquetas debatibles que dos casos ocultarían. Cada comando y resultado es real.",
            "date_modified": "2026-07-06T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "evals",
                "promptfoo",
                "inferencia",
                "datasets",
                "synthetic-data",
                "observabilidad"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/promptfoo-avatar-31b32c657d72dac0a7edbd77c24ec717.jpg\" alt=\"Promptfoo\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"data:image/png;base64,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\" alt=\"JSON\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 8 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->8</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->8<!-- --> earned</span></div><div class=\"skillTracker__series\">AI evals &amp; observability</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove a model works</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">2</span><span class=\"skillTracker__skill\" data-state=\"current\">Trustworthy JSON</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real test data at scale</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Observe &amp; score production</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">RAG, end to end</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">Catch regressions in CI</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trace &amp; debug agent tool calls</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/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>\"Devuelve solo JSON\" es una de las instrucciones más comunes en aplicaciones LLM en producción — y una de las menos fiables. Los modelos envuelven JSON en cercas de markdown, añaden una frase amigable, o (si son modelos de razonamiento) narran todo su proceso de pensamiento alrededor de ello. Cualquiera de esos casos rompe un estricto <code>JSON.parse</code>, y tu pipeline se cae.</p>\n<p>En esta guía construimos una evaluación de <a href=\"https://www.promptfoo.dev/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Promptfoo</a> que hace que los modelos de <strong>API de Inferencia WEC</strong> clasifiquen tickets de soporte en <strong>JSON validado por esquema</strong>, luego lo endurecemos contra el desorden del mundo real — y lo usamos para elegir un modelo en el que realmente puedes confiar. Esta es la parte 2 de la serie de evaluaciones (consulta <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/\">parte 1</a> para la configuración inicial). Todo aquí se ejecutó en vivo contra <code>https://inference.wiline.com</code>.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"cómo-funciona-la-evaluación\">Cómo funciona la evaluación<a href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/#c%C3%B3mo-funciona-la-evaluaci%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al Cómo funciona la evaluación\" title=\"Enlace directo al Cómo funciona la evaluación\" translate=\"no\">​</a></h2>\n<p>Cada prueba pasa por el mismo pipeline. El detalle que más importa: el <strong><code>transform</code> se ejecuta en la salida del modelo <em>antes</em> de que la afirmación la vea</strong> — eso es lo que nos permite recuperar JSON desordenado antes de validarlo.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">\n<p>Una <strong>clave de API de Inferencia WEC</strong> — consulta <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/cloud_portal/platform/inference/api_keys/\">Inferencia → Claves de API</a>.</p>\n</li>\n<li class=\"\">\n<p>Los nombres de los modelos utilizados aquí (<code>GLM-5.2</code>, <code>gemma4</code>, <code>Qwen2.5-3B-Instruct</code>, <code>qwen3.5:9B</code>) provienen del <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/cloud_portal/platform/inference/models_hub/\">Hub de Modelos</a> — intercambia por lo que hayas habilitado.</p>\n</li>\n<li class=\"\">\n<p><strong>Node.js 22+</strong> y tu clave exportada (Promptfoo se ejecuta a través de <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-tu-clave'</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>Si tu primera ejecución da error con <code>Cannot convert argument to a ByteString</code>, tu clave pegada tiene un carácter invisible — consulta <a href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/#ts-invisible-key\" class=\"\">Solución de problemas</a> al final.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--un-esquema-estricto-y-un-fallo-real\">Paso 1 — Un esquema estricto y un fallo real<a href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/#paso-1--un-esquema-estricto-y-un-fallo-real\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Un esquema estricto y un fallo real\" title=\"Enlace directo al Paso 1 — Un esquema estricto y un fallo real\" translate=\"no\">​</a></h2>\n<p>Haremos algo más cercano a una carga de trabajo real que \"capital de Francia\": clasificar un ticket de soporte en <code>categoría</code>, <code>prioridad</code> y un breve <code>resumen</code>. La mejora clave sobre una verificación básica es el <strong>Esquema JSON</strong> — impone <code>enum</code>s, por lo que un modelo que inventa una categoría falla.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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: \"API de Inferencia WEC — salida estructurada (esquema JSON)\"</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\">    Clasifica este ticket de soporte. Devuelve SOLO JSON con claves:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"categoría\" (una de: facturación, técnica, cuenta, otra),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"prioridad\" (una de: baja, media, alta),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"resumen\" (cadena, máximo 12 palabras).</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: [categoría, prioridad, resumen]</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\">          categoría: { type: string, enum: [facturación, técnica, cuenta, otra] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          prioridad: { type: string, enum: [baja, media, alta] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          resumen: { 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: \"Me cobraron dos veces por mi instancia este mes.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: facturación }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { ticket: \"Mi VM no arranca después de la última restauración de instantánea.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: técnica }]</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>El resultado — <strong>50%</strong>, y el fallo es instructivo:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Salida</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\">Me cobraron dos veces por mi instancia…      [FALLIDO] ```json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                          { \"categoría\": \"facturación\", \"prioridad\": \"alta\", … }</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\">Mi VM no arranca después de la última…        [APROBADO] { \"categoría\": \"técnica\", … }</span><br></div><div class=\"token-line\" style=\"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 aprobado (50%)   ✗ 1 fallido (50%)</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Paso 1 — un ticket falla porque el JSON está envuelto en cercas de markdown\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 1.</strong> Mismo modelo, mismo aviso, temperatura 0 — una respuesta es limpia, la otra está envuelta en <code>```json</code> cercas, por lo que <code>is-json</code> no puede analizarla.</p>\n<p>El <em>contenido</em> es correcto; el <em>formato</em> no lo es. <code>is-json</code> valida tanto \"¿es JSON analizable?\" como \"¿cumple con el esquema?\" — y la salida cercada falla la primera prueba.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--recuperar-salida-cercada-con-un-transform\">Paso 2 — Recuperar salida cercada con un <code>transform</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/#paso-2--recuperar-salida-cercada-con-un-transform\" class=\"hash-link\" aria-label=\"Enlace directo al paso-2--recuperar-salida-cercada-con-un-transform\" title=\"Enlace directo al paso-2--recuperar-salida-cercada-con-un-transform\" translate=\"no\">​</a></h2>\n<p>Un <code>transform</code> se ejecuta en la salida del modelo <strong>antes</strong> de las afirmaciones. Aquí eliminamos las cercas.\n(Este bloque utiliza cuatro comillas porque la configuración en sí contiene tres comillas).</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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: \"API de Inferencia WEC — transformación de eliminación de cercas\"</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\">    Clasifica este ticket de soporte. Devuelve SOLO JSON con claves:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"categoría\" (una de: facturación, técnica, cuenta, otra),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"prioridad\" (una de: baja, media, alta),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"resumen\" (cadena, máximo 12 palabras).</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: [categoría, prioridad, resumen]</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\">          categoría: { type: string, enum: [facturación, técnica, cuenta, otra] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          prioridad: { type: string, enum: [baja, media, alta] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          resumen: { 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: \"Me cobraron dos veces por mi instancia este mes.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: facturación }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { ticket: \"Mi VM no arranca después de la última restauración de instantánea.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: técnica }]</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>Ahora ambos pasan — <strong>2/2</strong>. Las cercas se eliminan antes de que <code>is-json</code> vea la salida.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Paso 2 — con la transformación de eliminación de cercas, ambos tickets pasan\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 2.</strong> El <code>transform</code> elimina las cercas de código, por lo que el ticket que antes fallaba se analiza.</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> necesita un <code>return</code> explícito</div><div class=\"admonitionContent_BuS1\"><p>Un <code>transform</code> de varias líneas es un cuerpo de función, no una expresión — omite <code>return</code> y obtienes <code>Transform function did not return a value</code>. Consulta <a href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/#ts-transform-return\" class=\"\">Solución de problemas</a>.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--la-solución-más-limpia-modo-json\">Paso 3 — La solución más limpia: modo JSON<a href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/#paso-3--la-soluci%C3%B3n-m%C3%A1s-limpia-modo-json\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — La solución más limpia: modo JSON\" title=\"Enlace directo al Paso 3 — La solución más limpia: modo JSON\" translate=\"no\">​</a></h2>\n<p>En lugar de limpiar después del modelo, pide a la API que emita solo JSON válido a través de <code>response_format</code>. Sin <code>transform</code> esta vez:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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: \"API de Inferencia WEC — modo JSON (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\">    Clasifica este ticket de soporte. Devuelve SOLO JSON con claves:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"categoría\" (una de: facturación, técnica, cuenta, otra),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"prioridad\" (una de: baja, media, alta),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"resumen\" (cadena, máximo 12 palabras).</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: [categoría, prioridad, resumen]</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\">          categoría: { type: string, enum: [facturación, técnica, cuenta, otra] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          prioridad: { type: string, enum: [baja, media, alta] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          resumen: { 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: \"Me cobraron dos veces por mi instancia este mes.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: facturación }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { ticket: \"Mi VM no arranca después de la última restauración de instantánea.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: técnica }]</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 APROBADO</strong> sin transformación — parece que el modo JSON funciona.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Paso 3 — el modo JSON pasa en GLM-5.2\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 3.</strong> Con <code>response_format: json_object</code>, GLM-5.2 devuelve JSON limpio, sin necesidad de transformación.</p>\n<p>Pero antes de confiar en <code>response_format</code>, necesitamos probarlo en todos los modelos — porque un solo resultado verde puede ser suerte, no cumplimiento.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--matriz-de-fiabilidad-entre-modelos-wec\">Paso 4 — Matriz de fiabilidad entre modelos WEC<a href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/#paso-4--matriz-de-fiabilidad-entre-modelos-wec\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — Matriz de fiabilidad entre modelos WEC\" title=\"Enlace directo al Paso 4 — Matriz de fiabilidad entre modelos WEC\" translate=\"no\">​</a></h2>\n<p>Ejecuta la configuración en modo JSON contra los cuatro modelos de chat. Este es el beneficio — una <strong>comparación de fiabilidad específica de WEC</strong> que no puedes obtener de una tabla de clasificación genérica.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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: \"API de Inferencia WEC — fiabilidad en modo JSON entre modelos\"</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\">    Clasifica este ticket de soporte. Devuelve SOLO JSON con claves:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"categoría\" (una de: facturación, técnica, cuenta, otra),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"prioridad\" (una de: baja, media, alta),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"resumen\" (cadena, máximo 12 palabras).</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: [categoría, prioridad, resumen]</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\">          categoría: { type: string, enum: [facturación, técnica, cuenta, otra] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          prioridad: { type: string, enum: [baja, media, alta] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          resumen: { 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: \"Me cobraron dos veces por mi instancia este mes.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: facturación }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { ticket: \"Mi VM no arranca después de la última restauración de instantánea.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: técnica }]</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 y Qwen2.5-3B pasan limpiamente; <strong>gemma4 y qwen3.5:9B vierten su razonamiento</strong>\n(\"Proceso de Pensamiento…\") en lugar de JSON — a pesar de que <code>response_format</code> está configurado.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Paso 4 — con modo JSON, dos modelos pasan y dos emiten razonamiento\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 4.</strong> <code>response_format</code> configurado en los cuatro — pero los dos modelos orientados al razonamiento lo ignoran.</p>\n<table><thead><tr><th>Modelo</th><th>Resultado</th><th>Tokens (2 llamadas)</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>❌❌ (razonamiento)</td><td>251</td></tr><tr><td><code>qwen3.5:9B</code></td><td>❌❌ (razonamiento)</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Ahora puedes <strong>medir qué modelos realmente honran el modo JSON</strong> — en lugar de confiar en la bandera.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5--verificar-está-response_format-haciendo-algo\">Paso 5 — Verificar: ¿está <code>response_format</code> haciendo algo?<a href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/#paso-5--verificar-est%C3%A1-response_format-haciendo-algo\" class=\"hash-link\" aria-label=\"Enlace directo al paso-5--verificar-está-response_format-haciendo-algo\" title=\"Enlace directo al paso-5--verificar-está-response_format-haciendo-algo\" translate=\"no\">​</a></h2>\n<p>No asumas que el razonamiento fue <em>causado</em> por el modo JSON. Vuelve a ejecutar la misma matriz <strong>sin</strong>\n<code>response_format</code> (usando la transformación de eliminación de cercas en su lugar) y compara:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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: \"API de Inferencia WEC — matriz sin 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\">    Clasifica este ticket de soporte. Devuelve SOLO JSON con claves:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"categoría\" (una de: facturación, técnica, cuenta, otra),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"prioridad\" (una de: baja, media, alta),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"resumen\" (cadena, máximo 12 palabras).</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: [categoría, prioridad, resumen]</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\">          categoría: { type: string, enum: [facturación, técnica, cuenta, otra] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          prioridad: { type: string, enum: [baja, media, alta] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          resumen: { 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: \"Me cobraron dos veces por mi instancia este mes.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: facturación }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { ticket: \"Mi VM no arranca después de la última restauración de instantánea.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: técnica }]</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>Sigue siendo <strong>4/8</strong> — los mismos modelos fallan. Así que <code>response_format</code> <strong>no cambió nada</strong>: en WEC hoy es efectivamente un no-op (el backend no está haciendo cumplir el modo JSON). El comportamiento de razonamiento pertenece a los modelos, no a la bandera.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Paso 5 — mismo 4/8 sin response_format, demostrando que fue un no-op\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 5.</strong> Resultado idéntico sin modo JSON — la bandera no estaba haciendo nada.</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>nota</div><div class=\"admonitionContent_BuS1\"><p>Lección que merece su propio llamado: <strong>no atribuyas un resultado a un cambio hasta que hayas probado el cambio en aislamiento.</strong> Una ejecución adicional convirtió una conclusión incorrecta en la correcta.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-6--una-transformación-más-robusta-extraer-el-json\">Paso 6 — Una transformación más robusta: extraer el JSON<a href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/#paso-6--una-transformaci%C3%B3n-m%C3%A1s-robusta-extraer-el-json\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 6 — Una transformación más robusta: extraer el JSON\" title=\"Enlace directo al Paso 6 — Una transformación más robusta: extraer el JSON\" translate=\"no\">​</a></h2>\n<p>Los modelos de razonamiento <em>sí</em> emiten JSON — simplemente está enterrado en prosa. En lugar de eliminar cercas,\n<strong>extrae el primer bloque <code>{ … }</code></strong> de cualquier parte de la salida:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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: \"API de Inferencia WEC — transformación de extracción de JSON\"</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\">    Clasifica este ticket de soporte. Devuelve SOLO JSON con claves:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"categoría\" (una de: facturación, técnica, cuenta, otra),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"prioridad\" (una de: baja, media, alta),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"resumen\" (cadena, máximo 12 palabras).</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: [categoría, prioridad, resumen]</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\">          categoría: { type: string, enum: [facturación, técnica, cuenta, otra] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          prioridad: { type: string, enum: [baja, media, alta] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          resumen: { 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: \"Me cobraron dos veces por mi instancia este mes.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: facturación }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { ticket: \"Mi VM no arranca después de la última restauración de instantánea.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: técnica }]</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> — un gran salto. gemma4 y Qwen2.5 recuperan completamente; qwen3.5:9B recupera uno de dos. El\núnico fallo restante es revelador: esa respuesta emitió JSON <strong>y luego siguió razonando</strong> (con\nmás llaves), por lo que la coincidencia codiciosa <code>{ … }</code> capturó en exceso y produjo JSON inválido.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Paso 6 — la extracción de JSON rescata la mayoría de los modelos (7/8)\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 6.</strong> Extraer el bloque JSON recupera los modelos habladores — pero es una heurística, no una garantía (nota el único fallo por captura excesiva).</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-7--verlo-en-el-navegador\">Paso 7 — Verlo en el navegador<a href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/#paso-7--verlo-en-el-navegador\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 7 — Verlo en el navegador\" title=\"Enlace directo al Paso 7 — Verlo en el navegador\" translate=\"no\">​</a></h2>\n<p>La tabla del terminal está bien para una lectura rápida, pero el informe web es donde una matriz como esta cobra vida:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_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>El panel en la parte superior te da la imagen de fiabilidad del modelo de un vistazo — las barras de <strong>Tasa de Aprobación (%)</strong> son las que debes observar aquí: tres modelos altos, <code>qwen3.5:9B</code> sentado en 50%.\nA continuación, cada proveedor es una columna que puedes expandir para ver la <strong>salida completa</strong> (así es como obtuvimos el fallo por captura excesiva en <a href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/#ts-reasoning-dump\" class=\"\">Figura 10</a> — el terminal lo trunca).</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El informe de Promptfoo: barras de Tasa de Aprobación por modelo y salida por caso expandible\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 7.</strong> El panel del informe — las barras de Tasa de Aprobación convierten la matriz de modelos en un ranking de un vistazo, y cualquier celda se expande a la salida completa.</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>nota</div><div class=\"admonitionContent_BuS1\"><p>Los otros gráficos del informe (el dispersor de aviso frente a aviso) brillan cuando estás <strong>A/B-testando\navisos</strong>, no modelos — un buen tema para una publicación posterior. Para una ejecución de un solo aviso y múltiples modelos como esta, las barras de Tasa de Aprobación son el gráfico que importa.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-8--bloquearlo-en-ci\">Paso 8 — Bloquearlo en CI<a href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/#paso-8--bloquearlo-en-ci\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 8 — Bloquearlo en CI\" title=\"Enlace directo al Paso 8 — Bloquearlo en CI\" translate=\"no\">​</a></h2>\n<p>Promptfoo sale con un código no cero cuando las pruebas fallan, así que una línea es una puerta (ponlo en un script o\n<code>Makefile</code>, nunca en tu shell interactivo):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_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\">\"✅ aprobado\"</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\">\"❌ evaluación fallida — bloqueando\"</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=\"La puerta bloqueando en una evaluación fallida\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 8.</strong> Un fallo real bloquea: 7/8 aprobados, pero la captura excesiva de <code>qwen3.5:9B</code> (del Paso 6)\ndispara la puerta — el código de salida no cero es la señal que CI usa para bloquear un PR.</p>\n<p>En 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 configurada como una variable CI/CD enmascarada — nunca comprometida.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p><strong>JSON validado por esquema es ahora una puerta CI</strong> — un cambio en el modelo o aviso que rompe el análisis nunca se envía.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-aprendimos\">Lo que aprendimos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/#lo-que-aprendimos\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que aprendimos\" title=\"Enlace directo al Lo que aprendimos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong>La elección del modelo domina.</strong> <code>GLM-5.2</code> y <code>Qwen2.5-3B</code> producen JSON válido por esquema de manera fiable\ny son ~8× más baratos que los modelos de razonamiento. Para salida estructurada en WEC, comienza allí.</li>\n<li class=\"\"><strong>Los modelos de razonamiento</strong> (<code>qwen3.5:9B</code>, y <code>gemma4</code> en el día que probamos) narran su pensamiento\ny cuestan mucho más — malas opciones para JSON estricto.</li>\n<li class=\"\"><strong>Un <code>transform</code> es tu red de seguridad</strong> — la eliminación de cercas recupera el caso común; la extracción de JSON\nrecupera la mayoría de las salidas habladoras — pero la extracción es una heurística que puede capturar en exceso.</li>\n<li class=\"\"><strong><code>response_format</code> es un no-op en WEC hoy</strong> — verificado al aislarlo. No confíes en ello;\nvalida en su lugar.</li>\n<li class=\"\"><strong>La evaluación es el punto.</strong> Cada una de estas sorpresas fue capturada automáticamente, antes de que\nllegara a producción.</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-de-problemas\">Solución de problemas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/#soluci%C3%B3n-de-problemas\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas\" title=\"Enlace directo al Solución de problemas\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"la-función-de-transformación-no-devolvió-un-valor\"><a id=\"ts-transform-return\"></a>\"La función de transformación no devolvió un valor\"<a href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/#la-funci%C3%B3n-de-transformaci%C3%B3n-no-devolvi%C3%B3-un-valor\" class=\"hash-link\" aria-label=\"Enlace directo al la-función-de-transformación-no-devolvió-un-valor\" title=\"Enlace directo al la-función-de-transformación-no-devolvió-un-valor\" translate=\"no\">​</a></h3>\n<p>Un <code>transform</code> de varias líneas se compila como un <strong>cuerpo de función</strong>, por lo que necesita un <code>return</code> explícito. <code>output.replace(...)</code> por sí solo no devuelve nada.</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();   # nota el `return`</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El error de retorno de transformación\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 9.</strong> Sin <code>return</code>, Promptfoo da error en lugar de transformar.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"un-modelo-devuelve-razonamiento-alrededor-del-json\"><a id=\"ts-reasoning-dump\"></a>Un modelo devuelve razonamiento alrededor del JSON<a href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/#un-modelo-devuelve-razonamiento-alrededor-del-json\" class=\"hash-link\" aria-label=\"Enlace directo al un-modelo-devuelve-razonamiento-alrededor-del-json\" title=\"Enlace directo al un-modelo-devuelve-razonamiento-alrededor-del-json\" translate=\"no\">​</a></h3>\n<p>Si <code>is-json</code> falla con <strong>\"Se esperaba que la salida fuera JSON válido\"</strong> y la respuesta contiene una\nnarración de \"Proceso de Pensamiento…\" / \"Revisión Final…\", estás usando un modelo de razonamiento. A veces emite <em>solo</em> razonamiento; a veces — como a continuación — emite JSON válido y luego <strong>sigue hablando</strong>, por lo que incluso la extracción del Paso 6 captura en exceso y el análisis falla. Opciones: cambia a\n<code>GLM-5.2</code> / <code>Qwen2.5-3B</code>, o acepta que la transformación de extracción es un esfuerzo de buena fe, no una garantía.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El caso fallido expandido en el informe — JSON seguido de razonamiento adicional\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 10.</strong> El caso fallido expandido en el informe: <code>qwen3.5:9B</code> devolvió JSON válido y\n<em>después siguió razonando</em> (\"7. Revisión Final…\"), por lo que la extracción capturó en exceso y la afirmación informa \"Se esperaba que la salida fuera JSON válido.\"</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"no-se-puede-convertir-el-argumento-a-un-bytestring\"><a id=\"ts-invisible-key\"></a>\"No se puede convertir el argumento a un ByteString\"<a href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/#no-se-puede-convertir-el-argumento-a-un-bytestring\" class=\"hash-link\" aria-label=\"Enlace directo al no-se-puede-convertir-el-argumento-a-un-bytestring\" title=\"Enlace directo al no-se-puede-convertir-el-argumento-a-un-bytestring\" translate=\"no\">​</a></h3>\n<p>Tu clave de API pegada contiene un carácter Unicode invisible. Elimínalo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash 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=\"El error de ByteString de un carácter clave invisible\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 11.</strong> Un carácter extraño en la clave activa el encabezado HTTP antes de que se envíe cualquier solicitud.</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=\"qué-sigue\">¿Qué sigue?<a href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al ¿Qué sigue?\" title=\"Enlace directo al ¿Qué sigue?\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong>Conjuntos de regresión DeepEval</strong> — convierte estas verificaciones en una ejecución de regresión versionada y con puerta CI.</li>\n<li class=\"\"><strong>Trazado</strong> — ve <em>por qué</em> una respuesta salió mal, no solo que falló.</li>\n</ul>\n<p>¿Tienes un caso de salida estructurada que aún se escapa? Esa es la próxima prueba que agregar.</p>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/",
            "title": "JSON confiable: valida el esquema de la salida estructurada de tu modelo",
            "summary": "Los LLM prometen JSON y entregan cercas de markdown y razonamiento. Construye una evaluación de Promptfoo que clasifique tickets de soporte en JSON validado por esquema en la API de Inferencia WEC — con transformaciones para recuperar salidas desordenadas, una matriz de fiabilidad del modelo y una puerta CI. Cada comando y resultado es real.",
            "date_modified": "2026-07-01T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "evals",
                "promptfoo",
                "inferencia",
                "json",
                "structured-output",
                "observabilidad"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/",
            "content_html": "\n<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/promptfoo-avatar-31b32c657d72dac0a7edbd77c24ec717.jpg\" alt=\"Promptfoo\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 8 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->8</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->8<!-- --> earned</span></div><div class=\"skillTracker__series\">AI evals &amp; observability</div><ul class=\"skillTracker__steps\"><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">1</span><span class=\"skillTracker__skill\" data-state=\"current\">Prove a model works</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trustworthy JSON</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/synthetic-eval-datasets-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real test data at scale</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/observe-production-langfuse-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Observe &amp; score production</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/rag-docs-assistant-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">RAG, end to end</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deepeval-regression-testing-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">Catch regressions in CI</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/component-tracing-agent-tool-calls-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trace &amp; debug agent tool calls</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/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>No enviarías código sin pruebas — pero la mayoría de los equipos envían funciones LLM con \"se ve bien para mí.\" <strong>Evaluaciones</strong> (evals) solucionan eso: defines casos de prueba y criterios de aprobación/fallo, luego mides tu modelo objetivamente — detectando regresiones, comparando modelos y bloqueando despliegues.</p>\n<p>En esta guía construirás un verdadero arnés de evaluación con <a href=\"https://www.promptfoo.dev/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Promptfoo</a> apuntando a la <strong>API de Inferencia WEC</strong> — el mismo punto final compatible con OpenAI que llamas desde tus aplicaciones. Todo aquí se ejecutó en vivo contra <code>https://inference.wiline.com</code>; las salidas son reales.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">\n<p>Una cuenta de <strong>WEC (WiLine Edge Cloud)</strong> y una <strong>clave API de Inferencia</strong> — consulta <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/cloud_portal/platform/inference/api_keys/\">Inferencia → Claves API</a>.</p>\n</li>\n<li class=\"\">\n<p><strong>Node.js 22+</strong>. Promptfoo considera que Node 20 está al final de su vida útil; con <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 preferido</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>Ejecutaremos Promptfoo con <code>npx</code> — nada que instalar globalmente.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La página de claves API del portal WiLine\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 1.</strong> Crea una clave en Inferencia → Claves API, luego cópiala.</p>\n<p>Pon tu clave en la terminal:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash 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>Si tu primera evaluación falla con <code>Cannot convert argument to a ByteString</code>, consulta <a href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/#1-bytestring-error-on-first-eval\" class=\"\">Solución de problemas #1</a> a continuación.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--ver-qué-modelos-tienes\">Paso 1 — Ver qué modelos tienes<a href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/#paso-1--ver-qu%C3%A9-modelos-tienes\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Ver qué modelos tienes\" title=\"Enlace directo al Paso 1 — Ver qué modelos tienes\" translate=\"no\">​</a></h2>\n<p>La API es compatible con OpenAI, por lo que la lista de modelos es un simple <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\">Salida</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>Los modelos <code>whisper-*</code> son de voz a texto; el resto son modelos de chat. Evaluaremos los modelos de chat, comenzando con <code>zai-org/GLM-5.2</code>.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--tu-primera-evaluación\">Paso 2 — Tu primera evaluación<a href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/#paso-2--tu-primera-evaluaci%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — Tu primera evaluación\" title=\"Enlace directo al Paso 2 — Tu primera evaluación\" translate=\"no\">​</a></h2>\n<p>Crea una configuración. <code>apiBaseUrl</code> apunta Promptfoo a WEC; <code>apiKeyEnvar</code> le dice qué variable de entorno contiene tu clave:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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: \"Evaluación de API de Inferencia WEC\"</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\">  - \"Responde en una sola palabra. ¿Cuál es la capital de {{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: primera evaluación, 2 casos ambos APROBADOS\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 4.</strong> Tu primera evaluación aprobada contra la API de Inferencia WEC.</p>\n<p><code>icontains</code> es una afirmación <strong>determinista</strong> — una verificación de subcadena que no distingue entre mayúsculas y minúsculas. Barata, rápida y perfecta para hechos con una respuesta conocida.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--el-informe-visual\">Paso 3 — El informe visual<a href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/#paso-3--el-informe-visual\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — El informe visual\" title=\"Enlace directo al Paso 3 — El informe visual\" 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>Esto abre una interfaz web local con cada aviso, salida y aprobación/fallo — mucho más fácil de escanear que la terminal una vez que tienes más de unos pocos casos.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El informe web de Promptfoo\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 5.</strong> <code>promptfoo view</code> — los mismos resultados en el navegador.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--agregar-un-segundo-modelo\">Paso 4 — Agregar un segundo modelo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/#paso-4--agregar-un-segundo-modelo\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — Agregar un segundo modelo\" title=\"Enlace directo al Paso 4 — Agregar un segundo modelo\" translate=\"no\">​</a></h2>\n<p>Agregar un proveedor es un bloque adicional. Aquí agregamos <code>Qwen2.5-3B-Instruct</code> junto a GLM-5.2 y ejecutamos los mismos dos casos de prueba contra ambos:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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: \"Evaluación de API de Inferencia WEC — comparación de modelos\"</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\">  - \"Responde en una sola palabra. ¿Cuál es la capital de {{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: comparación de 2 modelos, 4 casos todos APROBADOS\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 6.</strong> Ambos modelos aprueban. Los resultados de GLM-5.2 se sirven desde caché (0 nuevas solicitudes); solo Qwen2.5 realiza llamadas en vivo.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5--califica-calidad-no-subcadenas-rúbrica-llm\">Paso 5 — Califica calidad, no subcadenas (rúbrica LLM)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/#paso-5--califica-calidad-no-subcadenas-r%C3%BAbrica-llm\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 5 — Califica calidad, no subcadenas (rúbrica LLM)\" title=\"Enlace directo al Paso 5 — Califica calidad, no subcadenas (rúbrica LLM)\" translate=\"no\">​</a></h2>\n<p>Las verificaciones de subcadenas no pueden juzgar una respuesta abierta. Una afirmación <code>llm-rubric</code> <strong>calificada por el modelo</strong> utiliza un modelo para puntuar la salida contra criterios — y el calificador puede ejecutarse en WEC también, por lo que no necesitas un proveedor externo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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: \"Evaluación de API de Inferencia WEC — calificación por modelo (rúbrica LLM)\"</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:                       # el modelo calificador</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\">  - \"Explica qué es un {{topic}} en dos oraciones, para un principiante.\"</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: \"Describe una VPC como una red virtual privada e aislada en la nube, y es comprensible para un principiante.\"</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: evaluación de rúbrica LLM, 1 APROBADO, línea de tokens de calificación visible\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 7.</strong> La ejecución muestra dos líneas de tokens — <code>Eval</code> (75) y <code>Grading</code> (350) — porque la verificación de rúbrica es en sí misma una llamada de modelo.</p>\n<p>Las verificaciones calificadas por el modelo son poderosas, pero no son gratuitas.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-6--líneas-de-defensa-latencia\">Paso 6 — Líneas de defensa: latencia<a href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/#paso-6--l%C3%ADneas-de-defensa-latencia\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 6 — Líneas de defensa: latencia\" title=\"Enlace directo al Paso 6 — Líneas de defensa: latencia\" translate=\"no\">​</a></h2>\n<p>Una respuesta correcta que toma 30 segundos sigue siendo un problema de producción. Agrega un límite de <strong>latencia</strong> a <code>defaultTest</code> (se aplica a cada caso). Siempre ejecuta con <code>--no-cache</code> — las respuestas en caché informan ~0 ms y pasarían la puerta gratis:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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: \"Evaluación de API de Inferencia WEC — límite de latencia\"</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        # fallar cualquier cosa más lenta que 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\">  - \"Responde en una sola palabra. ¿Cuál es la capital de {{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>Esto <strong>falló</strong> — las respuestas eran correctas (<code>Paris</code>, <code>Tokyo</code>) pero ambas llamadas tomaron ~33 s, activando la puerta de 5 s:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal: latencia 5000ms umbral, 2 FALLIDOS, Duración: 33s\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 8.</strong> La línea de defensa se activa: respuestas correctas, latencia incorrecta. Duración: 33s.</p>\n<p>Aumenta el umbral a algo realista y vuelve a ejecutar:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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: latencia 60000ms umbral, 2 APROBADOS, Duración: 2s\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 9.</strong> Con un umbral de 60 s, las mismas llamadas pasan fácilmente — Duración: 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>La latencia de inferencia varía — calibra antes de bloquear</div><div class=\"admonitionContent_BuS1\"><p>Las llamadas consecutivas al mismo modelo variaron de ~2 s a ~33 s en el mismo día. Ejecuta algunos pases <code>--no-cache</code> para obtener una línea base realista antes de fijar tu umbral.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-7--líneas-de-defensa-latencia--límite-de-tokens\">Paso 7 — Líneas de defensa: latencia + límite de tokens<a href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/#paso-7--l%C3%ADneas-de-defensa-latencia--l%C3%ADmite-de-tokens\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 7 — Líneas de defensa: latencia + límite de tokens\" title=\"Enlace directo al Paso 7 — Líneas de defensa: latencia + límite de tokens\" translate=\"no\">​</a></h2>\n<p>La afirmación de <code>cost</code> no funciona contra WEC (consulta <a href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/#2-cost-assertion-errors-immediately\" class=\"\">Solución de problemas #2</a>). Usa latencia + <code>max_tokens</code> juntos como tu línea de defensa de producción en su lugar:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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: \"Evaluación de API de Inferencia WEC — líneas de defensa (latencia + límite de tokens)\"</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\">  - \"Responde en una sola palabra. ¿Cuál es la capital de {{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: latencia 30000ms + max_tokens:16, 2 APROBADOS\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 11.</strong> Con un umbral de 30 s y un límite de 16 tokens, ambos casos aprueban.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-8--validar-salida-estructurada-validación-de-esquema-json\">Paso 8 — Validar salida estructurada (validación de esquema JSON)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/#paso-8--validar-salida-estructurada-validaci%C3%B3n-de-esquema-json\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 8 — Validar salida estructurada (validación de esquema JSON)\" title=\"Enlace directo al Paso 8 — Validar salida estructurada (validación de esquema JSON)\" translate=\"no\">​</a></h2>\n<p>Las aplicaciones reales quieren JSON, no prosa. <code>is-json</code> verifica que la salida sea un JSON válido <strong>y</strong> coincida con un esquema en una afirmación:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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: \"Evaluación de API de Inferencia WEC — validación de esquema JSON\"</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\">  - 'Devuelve SOLAMENTE un objeto JSON (sin markdown, sin prosa) con claves \"capital\" (cadena) y \"population_millions\" (número) para el país {{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: evaluación de esquema JSON, Francia y Japón ambos APROBADOS con salida JSON limpia\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 12.</strong> GLM-5.2 devuelve JSON compacto y válido según el esquema en el primer intento.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-9--la-recompensa-compara-cada-modelo-wec\">Paso 9 — La recompensa: compara cada modelo WEC<a href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/#paso-9--la-recompensa-compara-cada-modelo-wec\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 9 — La recompensa: compara cada modelo WEC\" title=\"Enlace directo al Paso 9 — La recompensa: compara cada modelo WEC\" translate=\"no\">​</a></h2>\n<p>Aquí está lo que no puedes obtener en ningún otro lugar — la misma prueba estricta de JSON a través de <strong>todos</strong> los modelos de chat en tu cuenta:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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: \"Evaluación de API de Inferencia WEC — matriz de modelos\"</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\">  - 'Devuelve SOLAMENTE un objeto JSON (sin markdown, sin prosa) con claves \"capital\" (cadena) y \"population_millions\" (número) para el país {{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: tabla de resultados de matriz de 4 modelos — GLM, gemma4, Qwen2.5 todos APROBADOS; qwen3.5:9B todos FALLIDOS con texto de razonamiento\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 13a.</strong> 12 casos de prueba a través de 4 modelos — 9 aprobados, 3 fallidos. <code>qwen3.5:9B</code> emite su cadena de pensamiento en lugar de JSON.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal: resumen de tokens de matriz de modelos — qwen3.5:9B consumió 2,650 tokens frente a ~200 para los demás\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 13b.</strong> La descomposición de tokens cuenta la verdadera historia: el modelo de razonamiento costó 13× más para la misma tarea.</p>\n<table><thead><tr><th>Modelo</th><th>¿JSON Estricto?</th><th>Notas</th><th>Tokens (3 llamadas)</th></tr></thead><tbody><tr><td><code>zai-org/GLM-5.2</code></td><td>✅ todos aprueban</td><td>JSON compacto</td><td>177</td></tr><tr><td><code>gemma4</code></td><td>✅ todos aprueban</td><td>poblaciones enteras</td><td>207</td></tr><tr><td><code>Qwen2.5-3B-Instruct</code></td><td>✅ todos aprueban</td><td>JSON bien formateado, preciso</td><td>262</td></tr><tr><td><code>qwen3.5:9B</code></td><td>❌ todos fallan</td><td><strong>modelo de razonamiento</strong> — emite su \"pensamiento\" en lugar de JSON crudo</td><td><strong>2,650</strong></td></tr></tbody></table>\n<p>Dos lecciones se desprenden de inmediato:</p>\n<ul>\n<li class=\"\"><strong><code>qwen3.5:9B</code> es un modelo de razonamiento.</strong> \"Piensa en voz alta\", por lo que rompe el JSON estricto a menos que elimines el razonamiento o uses una restricción de formato de respuesta. Elegirlo para salida estructurada fallaría silenciosamente.</li>\n<li class=\"\"><strong>El costo varía ~13×.</strong> El modelo de razonamiento consumió 2,650 tokens para la misma tarea que los demás hicieron en ~200. Para un trabajo de extracción de JSON, los modelos más pequeños son correctos <em>y</em> mucho más baratos.</li>\n</ul>\n<p>Esa es una decisión de selección de modelo que ahora puedes defender con datos — para <em>tus</em> cargas de trabajo, no una tabla de clasificación genérica.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Ahora puedes <strong>comparar cada modelo en tu punto final con números objetivos y repetibles</strong> — precisión, latencia y costo lado a lado.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-10--hazlo-una-puerta\">Paso 10 — Hazlo una puerta<a href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/#paso-10--hazlo-una-puerta\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 10 — Hazlo una puerta\" title=\"Enlace directo al Paso 10 — Hazlo una puerta\" translate=\"no\">​</a></h2>\n<p>Promptfoo sale <strong>no cero (<code>100</code>)</strong> cuando cualquier prueba falla. Verifica esto primero:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_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\">\"código de salida: </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: código de salida: 100\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 14.</strong> El código de salida 100 confirma que Promptfoo señala fallos de manera confiable — exactamente lo que necesita CI.</p>\n<p>Conéctalo como una puerta:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_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\">\"✅ evaluación aprobada\"</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\">\"❌ evaluación fallida — bloqueando\"</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: tabla de evaluación de puerta CI — mismos resultados de 4 modelos, 9 aprobados / 3 fallidos\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 15a.</strong> La puerta vuelve a ejecutar la evaluación de matriz desde caché — mismo resultado 9/3.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal: ❌ evaluación fallida — bloqueando\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 15b.</strong> Promptfoo sale no cero, se activa la rama <code>||</code>, y la puerta bloquea.</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>Nunca ejecutes la variante <code>exit 1</code> en tu terminal interactiva — consulta <a href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/#3-exit-1-closes-your-ssh-session\" class=\"\">Solución de problemas #3</a>.</p></div></div>\n<p>En CI, ni siquiera necesitas el <code>exit 1</code> — la salida no cero falla el trabajo. En 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 se establece como una variable CI/CD enmascarada — nunca la cometas.</span><br></div></code></pre></div></div>\n<p>Ahora un cambio de aviso que silenciosamente rompe la salida JSON, o un cambio de modelo que duplica la latencia, falla la canalización antes de que llegue a los usuarios.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Tu evaluación es ahora una <strong>puerta de despliegue</strong> — las regresiones fallan CI en lugar de llegar a los usuarios.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-de-problemas-errores-reales\">Solución de problemas (errores reales)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/#soluci%C3%B3n-de-problemas-errores-reales\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas (errores reales)\" title=\"Enlace directo al Solución de problemas (errores reales)\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"1-error-de-bytestring-en-la-primera-evaluación\">1. Error de ByteString en la primera evaluación<a href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/#1-error-de-bytestring-en-la-primera-evaluaci%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al 1. Error de ByteString en la primera evaluación\" title=\"Enlace directo al 1. Error de ByteString en la primera evaluación\" 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\">Salida</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 de llamada a la API: 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: error de ByteString, ambos casos ERROR\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 2.</strong> Cada llamada a la API falla antes de llegar al servidor — la clave nunca siquiera sale de la máquina.</p>\n<p><strong>Causa:</strong> La clave pegada contiene un carácter Unicode invisible — U+2028 (separador de línea). Algunos terminales y administradores de portapapeles lo insertan silenciosamente al copiar de ciertas fuentes.</p>\n<p><strong>Solución:</strong> Elimina los caracteres no imprimibles de la clave:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash 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: comando de eliminación de clave + confirmación de len=25\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 3.</strong> Después de eliminar, la longitud de la clave confirma que no quedan caracteres adicionales. Vuelve a ejecutar la evaluación — pasa.</p>\n<hr>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"2-errores-de-afirmación-de-costo-inmediatamente\">2. Errores de afirmación de costo inmediatamente<a href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/#2-errores-de-afirmaci%C3%B3n-de-costo-inmediatamente\" class=\"hash-link\" aria-label=\"Enlace directo al 2. Errores de afirmación de costo inmediatamente\" title=\"Enlace directo al 2. Errores de afirmación de costo inmediatamente\" 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\">Salida</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: La afirmación de costo no admite proveedores que no devuelven costo</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal: afirmación de costo, 2 errores\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 10.</strong> La API de Inferencia WEC no devuelve un campo de costo en sus respuestas.</p>\n<p><strong>Causa:</strong> Promptfoo calcula el costo buscando un precio por token para el modelo en su lista de precios incorporada y multiplicando por los tokens utilizados. Tiene precios para los grandes modelos alojados pero no para los nombres de modelos autoalojados de WEC, por lo que no tiene un precio que aplicar y la afirmación falla. (La mayoría de las API, incluida OpenAI, no devuelven un costo en dólares en la respuesta — las herramientas lo calculan).</p>\n<p><strong>Solución:</strong> Omite la afirmación de <code>cost</code> por completo. Realiza un seguimiento del gasto desde la línea <code>Total Tokens</code> que Promptfoo imprime al final de cada ejecución, y usa <code>max_tokens</code> en la configuración del proveedor para limitar el uso de tokens por llamada.</p>\n<hr>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"3-exit-1-cierra-tu-sesión-ssh\">3. <code>exit 1</code> cierra tu sesión SSH<a href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/#3-exit-1-cierra-tu-sesi%C3%B3n-ssh\" class=\"hash-link\" aria-label=\"Enlace directo al 3-exit-1-cierra-tu-sesión-ssh\" title=\"Enlace directo al 3-exit-1-cierra-tu-sesión-ssh\" 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\">Salida</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\">❌ evaluación fallida — bloqueando</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\">Conexión a 10.80.4.212 cerrada.</span><br></div></code></pre></div></div>\n<p><strong>Causa:</strong> Ejecutar <code>|| { echo \"❌ evaluación fallida — bloqueando\"; exit 1; }</code> directamente en una terminal interactiva cierra el proceso de la terminal en sí — lo que, a través de SSH, cierra la conexión.</p>\n<p><strong>Solución:</strong> Coloca la puerta en un contexto no interactivo: un objetivo de <code>Makefile</code>, un script de <code>npm</code>, un gancho <code>pre-commit</code> de git, o un trabajo de CI. En esos entornos, <code>exit 1</code> falla el trabajo sin tocar tu terminal. Si necesitas probar la puerta localmente, usa <code>|| echo \"❌ evaluación fallida\"</code> (sin <code>exit 1</code>) para ver la salida de forma segura.</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=\"qué-sigue\">¿Qué sigue?<a href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al ¿Qué sigue?\" title=\"Enlace directo al ¿Qué sigue?\" translate=\"no\">​</a></h2>\n<p>Has pasado de \"se ve bien para mí\" a un verdadero arnés de evaluación automatizable en WEC — con verificaciones deterministas, líneas de defensa, validación de esquemas, rúbricas calificadas por modelos, una matriz de modelos, y una puerta CI.</p>\n<p>Próximamente en esta serie:</p>\n<ul>\n<li class=\"\"><strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/structured-output-evals-wiline-inference/\">JSON confiable: valida el esquema de la salida estructurada de tu modelo</a></strong>\n— <strong>ahora disponible (parte 2):</strong> recupera la salida envuelta en cercas y razonamiento con transformaciones, compara modelos por confiabilidad JSON, y bloquea todo en CI.</li>\n<li class=\"\"><strong>Evaluaciones calificadas por modelos bien hechas</strong> — calibrando <code>llm-rubric</code>, puntuación de factualidad, y verificaciones de múltiples jueces para que tu calificador sea confiable.</li>\n<li class=\"\"><strong>Capturando alucinaciones</strong> — emparejando estas técnicas con la <a href=\"https://wec.wiline.com/resources/rag-hallucination-eval\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">evaluación de alucinaciones RAG</a> de WEC.</li>\n<li class=\"\"><strong>Evaluando audio</strong> — puntuando la precisión de la transcripción de Whisper en la API de Inferencia WEC.</li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/",
            "title": "Evalúa tus modelos con Promptfoo en la API de Inferencia WEC",
            "summary": "Deja de estimar la salida de LLM. Construye un verdadero arnés de evaluación con Promptfoo apuntando a la API de Inferencia WEC — afirmaciones, límites de latencia, verificaciones de esquema JSON, rúbricas calificadas por el modelo, una comparación de modelos totalmente WEC, y una puerta CI. Cada comando y resultado es real.",
            "date_modified": "2026-06-30T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "evals",
                "promptfoo",
                "inferencia",
                "testing",
                "observabilidad"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/run-openclaw-on-wec-models/",
            "content_html": "<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 6 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->6</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->6<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting OpenClaw</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Deploy your own AI assistant</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real HTTPS + auth</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Chat from Telegram</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Private mesh access</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">5</span><span class=\"skillTracker__skill\" data-state=\"current\">Run it on WEC models</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/add-whatsapp-channel-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Chat from WhatsApp</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/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>A través de <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/\">Partes 1–4</a> implementaste\nOpenClaw, lo aseguraste con HTTPS, añadiste Telegram y lo hiciste privado a través de una\nmalla NetBird — todo apuntando a <strong>OpenAI</strong>. Esta parte intercambia el modelo por debajo:\napunta el mismo agente a <strong>Modelos WEC</strong> — la inferencia compatible con OpenAI de WiLine —\ny ejecútalo en un modelo de peso abierto, <strong>Llama 3.1 8B Instruct</strong>. Misma caja, sin reconstrucción\n— solo un cambio de base-URL, clave y modelo a través de la CLI de OpenClaw.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>Continúa en la <strong>misma Instancia WEC</strong> de las Partes 1–4. Modelos WEC es un\npunto final <strong>compatible con OpenAI</strong> en <code>https://inference.wiline.com/v1</code>; cada comando\na continuación se ejecutó contra un contenedor de puerta de enlace OpenClaw en vivo\n(<code>openclaw-openclaw-gateway-1</code>).</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"por-qué-apuntar-openclaw-a-modelos-wec\">Por qué apuntar OpenClaw a Modelos WEC<a href=\"https://development-wec.wiline.com/docs/es/tutorials/run-openclaw-on-wec-models/#por-qu%C3%A9-apuntar-openclaw-a-modelos-wec\" class=\"hash-link\" aria-label=\"Enlace directo al Por qué apuntar OpenClaw a Modelos WEC\" title=\"Enlace directo al Por qué apuntar OpenClaw a Modelos WEC\" translate=\"no\">​</a></h2>\n<p>Si autoalojaste el agente para empezar, ya te importa el control — no\ndepender de la nube de otra persona, no estar a un correo de la página de precios de\nreconstrucción. Pero hay una brecha en esa historia: el agente se ejecuta en tu caja, y cada\nmensaje que maneja todavía se envía a OpenAI para que lo procese. Autoalojar\nel agente y externalizar el modelo es solo la mitad del trabajo. Esta parte lo cierra.</p>\n<ul>\n<li class=\"\"><strong>Un modelo, de extremo a extremo en WiLine</strong> — tu agente <em>y</em> su modelo se ejecutan en WEC; sin\nproveedor de terceros en la ruta caliente.</li>\n<li class=\"\"><strong>Pesos abiertos</strong> — Llama 3.1 no es una caja cerrada; no estás bloqueado a una sola\nAPI de proveedor para mantener el agente en funcionamiento.</li>\n<li class=\"\"><strong>Es un cambio de configuración, no una migración</strong> — OpenClaw ya habla la API de OpenAI,\ny Modelos WEC es compatible con OpenAI, por lo que mantienes tu agente, prompts y\ncanales intactos.</li>\n</ul>\n<p>También es más barato. El catálogo propio de WEC fija el precio de <code>Llama3.1-8B-Instruct</code> en\n<strong>$0.15 / 1M tokens de entrada · $0.20 / 1M tokens de salida</strong>. OpenAI no publica una\ntarifa independiente para <code>gpt-5.5</code> (el modelo que OpenClaw utiliza por defecto en\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/\">Parte 1</a>) más, pero su miembro familiar más cercano\nactual — GPT-5.6 \"Terra\", presentado por OpenAI como\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/news/gpt-5-6-token-economics/\">\"competitivo en rendimiento con GPT-5.5\"</a> — se lista en\n<strong>$2.50 / 1M en · $15 / 1M fuera</strong>. Esa es una comparación aproximada, no un\nbenchmark de manzanas a manzanas — el movimiento honesto, como siempre en este blog, es\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/\">evalúalo en tu propia carga de trabajo</a>\nen lugar de confiar en el número de ningún proveedor. Pero la dirección es lo suficientemente clara como para valer los diez minutos que esto toma.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/run-openclaw-on-wec-models/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">OpenClaw ejecutándose desde <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/\">Partes 1–4</a> (cualquiera\nde las configuraciones funciona). Esta guía asume que el contenedor de puerta de enlace se llama\n<code>openclaw-openclaw-gateway-1</code>.</li>\n<li class=\"\">Una <strong>clave API de WEC</strong>.</li>\n<li class=\"\">Acceso a la terminal de la caja de OpenClaw.</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--obtener-una-clave-api-de-wec\">Paso 1 — Obtener una clave API de WEC<a href=\"https://development-wec.wiline.com/docs/es/tutorials/run-openclaw-on-wec-models/#paso-1--obtener-una-clave-api-de-wec\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Obtener una clave API de WEC\" title=\"Enlace directo al Paso 1 — Obtener una clave API de WEC\" translate=\"no\">​</a></h2>\n<p>Consulta <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/cloud_portal/platform/inference/api_keys/\">Inferencia → Claves API</a> si no\ntienes una (la misma clave de\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/\">Parte 1 de la serie de evaluaciones</a>\nfunciona bien aquí).</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La página de Claves API del portal de WiLine\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 1.</strong> Crea una clave en Inferencia → Claves API, luego cópiala.</p>\n<p>Mantenla fuera de tu historial de terminal — ponla en un archivo en lugar de exportarla\ndirectamente:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash 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=\"paso-2--encontrar-el-id-del-modelo\">Paso 2 — Encontrar el ID del modelo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/run-openclaw-on-wec-models/#paso-2--encontrar-el-id-del-modelo\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — Encontrar el ID del modelo\" title=\"Enlace directo al Paso 2 — Encontrar el ID del modelo\" translate=\"no\">​</a></h2>\n<p>Consulta el punto final compatible con OpenAI <code>/v1/models</code> para ver qué está activo en tu Instancia WEC:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>La respuesta lista cada modelo disponible para tu clave, junto con el id que\nutiliza este tutorial:</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>Los ids de modelo en WEC no siempre son el nombre de visualización mostrado en el portal — siempre toma\nel <code>id</code> exacto de esta respuesta, no una suposición.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--apuntar-openclaw-a-modelos-wec\">Paso 3 — Apuntar OpenClaw a Modelos WEC<a href=\"https://development-wec.wiline.com/docs/es/tutorials/run-openclaw-on-wec-models/#paso-3--apuntar-openclaw-a-modelos-wec\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — Apuntar OpenClaw a Modelos WEC\" title=\"Enlace directo al Paso 3 — Apuntar OpenClaw a Modelos WEC\" translate=\"no\">​</a></h2>\n<p>No edites manualmente <code>openclaw.json</code> — usa <code>openclaw config set</code>. Valida el\narchivo completo en cada escritura, por lo que un error tipográfico falla ruidosamente en lugar de romper\nsilenciosamente la puerta de enlace. Un proveedor personalizado necesita su base URL, su estilo de API y su lista de modelos declarados <strong>juntos</strong>, en una sola escritura — el esquema rechaza un proveedor que\nfalta alguno de los tres, así que construye el objeto completo desde el principio:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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>Eso registra WEC como un proveedor, pero aún no está autenticado. No pongas la\nclave directamente en ese JSON — la configuración de composición de OpenClaw mantiene las claves API fuera de\nlas variables de entorno por completo y las almacena en un directorio de secretos dedicado respaldado por archivos en su lugar (<code>OPENCLAW_AUTH_PROFILE_SECRET_DIR</code>, montado en el\ncontenedor). Pasa la clave al propio almacén de autenticación de OpenClaw y deja que haga la conexión:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash 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\">Salida</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>Verifica que el proveedor y su autenticación se resolvieron juntos, luego haz que el modelo\nsea el predeterminado para que cada agente lo recoja sin configuración adicional:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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\">Modelo                                      Entrada      Ctx         Autenticación Local  Etiquetas</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">wec/Llama3.1-8B-Instruct                   texto       200k        no    sí   configurado</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">openai/gpt-5.5                             texto       200k        no    sí   configurado,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\">Modelo predeterminado: wec/Llama3.1-8B-Instruct</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal mostrando la lista de modelos de openclaw con wec/Llama3.1-8B-Instruct configurado y establecido como modelo predeterminado\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 2.</strong> <code>wec/Llama3.1-8B-Instruct</code>, autenticado y establecido como el modelo predeterminado.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--verificar\">Paso 4 — Verificar<a href=\"https://development-wec.wiline.com/docs/es/tutorials/run-openclaw-on-wec-models/#paso-4--verificar\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — Verificar\" title=\"Enlace directo al Paso 4 — Verificar\" translate=\"no\">​</a></h2>\n<p>OpenClaw puede ejecutar más de un agente por puerta de enlace, así que cada mensaje necesita un\nobjetivo — pasa <code>--agent</code>, coincidiendo con lo que <code>openclaw agents list</code> llama tu\nagente (<code>main</code>, a menos que lo hayas renombrado):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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\">Agentes:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">- main (predeterminado)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  Espacio de trabajo: $OPENCLAW_HOME/.openclaw/workspace</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  Modelo: wec/Llama3.1-8B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  Reglas de enrutamiento: 0</span><br></div></code></pre></div></div>\n<p>Envíale un mensaje y ve quién responde:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">\"¿Cuál es la capital de Francia?\"</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\">Salida</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\">París es la capital de Francia.</span><br></div></code></pre></div></div>\n<p>Eso es WEC, en Llama 3.1, sin OpenAI en el circuito.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal mostrando el agente respondiendo &amp;quot;París es la capital de Francia.&amp;quot; después de cambiar a WEC\" src=\"https://development-wec.wiline.com/docs/es/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>Figura 3.</strong> El agente, ejecutándose en Llama 3.1 8B Instruct a través de WEC.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5-opcional--desactivar-el-modo-de-pensamiento\">Paso 5 (opcional) — Desactivar el modo de pensamiento<a href=\"https://development-wec.wiline.com/docs/es/tutorials/run-openclaw-on-wec-models/#paso-5-opcional--desactivar-el-modo-de-pensamiento\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 5 (opcional) — Desactivar el modo de pensamiento\" title=\"Enlace directo al Paso 5 (opcional) — Desactivar el modo de pensamiento\" translate=\"no\">​</a></h2>\n<p>¿Quieres que sea más rápido? Desactiva el modo de pensamiento — es un comando de barra dentro del\nchat, no un comando de terminal:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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\">El modo de pensamiento ha sido desactivado.</span><br></div></code></pre></div></div>\n<p>O haz que se mantenga para cada sesión:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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 se necesita reiniciar la puerta de enlace.</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-de-problemas-real\">Solución de problemas (real)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/run-openclaw-on-wec-models/#soluci%C3%B3n-de-problemas-real\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas (real)\" title=\"Enlace directo al Solución de problemas (real)\" translate=\"no\">​</a></h2>\n<p><strong>\"los proveedores de modelos personalizados deben declarar modelos\"</strong> — declara <code>baseUrl</code>, <code>api</code>,\ny <code>models</code> juntos, en una sola llamada <code>config set</code>. Un proveedor personalizado que\nfalta alguno de los tres es rechazado; no puedes añadir <code>models</code> en una escritura posterior.</p>\n<p><strong>\"models.providers.wec.apiKey no está resuelto en la instantánea de tiempo de ejecución activa\"</strong>\n— no establezcas <code>apiKey</code> en el objeto del proveedor; deja que <code>models auth paste-api-key</code>\nlo posea (Paso 3). ¿Ya estableciste uno? Elimínalo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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 se seleccionó ninguna sesión de destino\"</strong> — siempre pasa <code>--agent &lt;id&gt;</code> con\n<code>openclaw agent</code>. Verifica el id con <code>openclaw agents list</code> primero si no estás\nseguro (<code>main</code>, a menos que lo hayas renombrado).</p>\n<p><strong><code>-bash: /think: No such file or directory</code></strong> — <code>/think</code> es un comando de barra\npara dentro del chat, no un comando de terminal. Pásalo a través de <code>--message</code> en su lugar,\ncomo en el Paso 5.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-sigue\">Qué sigue<a href=\"https://development-wec.wiline.com/docs/es/tutorials/run-openclaw-on-wec-models/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al Qué sigue\" title=\"Enlace directo al Qué sigue\" translate=\"no\">​</a></h2>\n<p>Con el agente ejecutándose en nuestro propio modelo, el siguiente paso es <strong>medirlo</strong> —\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/eval-models-promptfoo-wiline-inference/\">evaluando la calidad de las respuestas con Promptfoo + Modelos WEC</a>.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Run it on WEC models</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/run-openclaw-on-wec-models/",
            "title": "Ejecutar OpenClaw en Modelos WEC",
            "summary": "Intercambia el proveedor cerrado por la propia inferencia de WiLine: apunta tu agente OpenClaw autoalojado a Modelos WEC — misma caja, un cambio de base-URL/clave/modelo, sin bloqueo por token.",
            "date_modified": "2026-06-25T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "autoalojamiento",
                "openclaw",
                "modelos-wec",
                "inferencia"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/hermesagent-c76bbe81610ae2e70b0950e0f96bec8a.png\" alt=\"Hermes Agent\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/SQLite370-e02c30e84f9e098787bb57701b0b7232.png\" alt=\"SQLite\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 2 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->2</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->2<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting Hermes</div><ul class=\"skillTracker__steps\"><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">1</span><span class=\"skillTracker__skill\" data-state=\"current\">Agent with persistent memory</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/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> es el agente de IA de código abierto\n(MIT) de Nous Research — \"el agente que crece contigo.\" Su característica destacada es\n<strong>memoria persistente</strong>: aprende sobre tus proyectos y <strong>no olvida entre reinicios</strong>. Esta guía lo despliega en la <strong>misma Instancia WEC</strong> que ya usas para\nOpenClaw, lo apunta a un modelo y prueba que la memoria sobrevive a un reinicio completo.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>Funciona en la <strong>misma Instancia WEC</strong> de la serie OpenClaw — sin nueva VM. El instalador\ndescarga su propio Python (a través de <code>uv</code>), herramientas de navegador Node.js y un Chromium\nde Playwright automáticamente. El estado se guarda en <code>~/.hermes/</code>. La puerta de enlace de mensajería se ejecuta como un <strong>servicio de usuario systemd</strong>, por lo que coexiste con la pila Docker de OpenClaw. Capturado en vivo en Ubuntu\n22.04 con <code>hermes</code> <strong>v0.17.0</strong>.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"por-qué-importa-la-memoria-persistente\">Por qué importa la memoria persistente<a href=\"https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/#por-qu%C3%A9-importa-la-memoria-persistente\" class=\"hash-link\" aria-label=\"Enlace directo al Por qué importa la memoria persistente\" title=\"Enlace directo al Por qué importa la memoria persistente\" translate=\"no\">​</a></h2>\n<p>Un agente que olvida en cada reinicio es un juguete. Hermes mantiene un <strong>almacenamiento duradero en\ndisco</strong> bajo <code>~/.hermes/</code>:</p>\n<ul>\n<li class=\"\"><strong><code>~/.hermes/memories/MEMORY.md</code></strong> — un archivo legible por humanos de hechos que el agente ha\ndecidido recordar.</li>\n<li class=\"\"><strong><code>~/.hermes/state.db</code></strong> — una base de datos <strong>SQLite</strong> que contiene cada sesión y mensaje,\ncon índices de <strong>búsqueda de texto completo FTS5</strong> superpuestos para que el agente pueda buscar su propia\nhistoria.</li>\n</ul>\n<p>Un despliegue, un fallo o un reinicio no borran lo que aprendió. Esa es la diferencia entre\nuna demostración de chat y un agente en el que puedes confiar día a día — y es exactamente lo que probamos al\nfinal.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"las-partes-móviles\">Las partes móviles<a href=\"https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/#las-partes-m%C3%B3viles\" class=\"hash-link\" aria-label=\"Enlace directo al Las partes móviles\" title=\"Enlace directo al Las partes móviles\" translate=\"no\">​</a></h2>\n<p>Todo se ejecuta en una Instancia WEC. Hermes se comunica con un modelo a través de la API de Inferencia WEC,\nmantiene su memoria en disco local y se ejecuta como un servicio de usuario systemd — por lo que se reinicia solo\ndespués de un reinicio y su memoria sigue ahí:</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">Una <strong>Instancia WEC</strong> existente con acceso a la shell (la caja de OpenClaw funciona).</li>\n<li class=\"\">Un modelo: ya sea un inicio de sesión en <strong>Nous Portal</strong>, o tu propia clave de API de proveedor (usamos una\nclave de OpenAI aquí; <strong>Modelos WEC</strong> con <code>glm-5.2</code> también funcionan).</li>\n<li class=\"\">Nada que preinstalar — el instalador maneja Python, Node.js y el motor\ndel navegador.</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--instalar-hermes\">Paso 1 — Instalar Hermes<a href=\"https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/#paso-1--instalar-hermes\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Instalar Hermes\" title=\"Enlace directo al Paso 1 — Instalar 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=\"Ejecutando la línea de instalación — el instalador de Hermes comienza a resolver y descargar paquetes\" src=\"https://development-wec.wiline.com/docs/es/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>El instalador resuelve un entorno virtual de Python con <code>uv</code>, instala las herramientas de navegador de Node.js, descarga un Chromium de Playwright, sincroniza las habilidades empaquetadas y coloca un\nlanzador <code>hermes</code> en <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\">Salida</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\">✓ Paquete principal instalado (verificado por hash a través de uv.lock)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">✓ Todas las dependencias instaladas</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">✓ Configuración del motor de navegador completa</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">✓ Lanzador hermes instalado → ~/.local/bin/hermes</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">✓ Directorio de configuración listo: ~/.hermes/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">→ Iniciando asistente de configuración...</span><br></div></code></pre></div></div>\n<p>Luego lanza automáticamente el asistente de configuración. Recarga tu shell después para que el\ncomando <code>hermes</code> esté en tu <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=\"paso-2--ejecutar-el-asistente-de-configuración\">Paso 2 — Ejecutar el asistente de configuración<a href=\"https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/#paso-2--ejecutar-el-asistente-de-configuraci%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — Ejecutar el asistente de configuración\" title=\"Enlace directo al Paso 2 — Ejecutar el asistente de configuración\" translate=\"no\">​</a></h2>\n<p>El asistente guía a través del tipo de configuración, backend de terminal y mensajería. En una caja que\nya ejecutó OpenClaw, ofrece <strong>importar</strong> la configuración antigua primero — puedes previsualizar\nlo que se migraría antes de que cambie algo.</p>\n<p>La primera elección es <strong>cómo configurar Hermes</strong>:</p>\n<ul>\n<li class=\"\"><strong>Configuración Rápida (Nous Portal)</strong> — un inicio de sesión OAuth, sin claves de API. El Portal agrupa un\nmodelo y una puerta de enlace de herramientas. <strong>Requiere una suscripción activa a Nous Portal.</strong></li>\n<li class=\"\"><strong>Configuración completa (trae tus propias claves)</strong> — configuras proveedores y pegas tus propias\nclaves.</li>\n</ul>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El aviso &amp;quot;¿Cómo te gustaría configurar Hermes?&amp;quot; con opciones de Configuración Rápida, Configuración completa y Pizarra en blanco\" src=\"https://development-wec.wiline.com/docs/es/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>Lo que realmente hicimos</div><div class=\"admonitionContent_BuS1\"><p>Elegimos <strong>Configuración Rápida</strong> primero. El inicio de sesión en el Portal mostró un código de dispositivo y se quedó en\n<em>\"Esperando aprobación (sondeando)\"</em> — necesita una suscripción activa al Portal, que esta\ncuenta no tenía. Presionamos <strong>Ctrl+C</strong> para cancelar y en su lugar <strong>trajimos nuestro propio\nmodelo en el Paso 3</strong> (a continuación) con una clave de OpenAI. Si <em>tienes</em> un plan de Portal, completa\nel inicio de sesión aquí y puedes omitir el Paso 3.</p></div></div>\n<p>Para el resto del asistente:</p>\n<ul>\n<li class=\"\"><strong>Backend de terminal:</strong> <em>Local</em> — los comandos se ejecutan directamente en esta máquina.</li>\n<li class=\"\"><strong>Mensajería:</strong> <em>Omitir</em> por ahora (puedes conectar Telegram/Discord más tarde con\n<code>hermes setup gateway</code>).</li>\n</ul>\n<p>El asistente aplica valores predeterminados sensatos para el agente (máximo de iteraciones, un umbral de compresión de contexto del 50%) y termina con un resumen de disponibilidad de herramientas.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--conectar-un-modelo\">Paso 3 — Conectar un modelo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/#paso-3--conectar-un-modelo\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — Conectar un modelo\" title=\"Enlace directo al Paso 3 — Conectar un modelo\" translate=\"no\">​</a></h2>\n<p>Fuera de la caja, el modelo predeterminado se enruta a través del Nous Portal / OpenRouter y necesita\nuna clave. Para usar <strong>tu propio proveedor</strong> en su lugar, ejecuta:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_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>Luego selecciona <strong>OpenAI ▸ OpenAI API</strong>, pega tu clave de API cuando se te pida (se escribe en\n<code>~/.hermes/.env</code> — nunca se muestra), acepta la URL base predeterminada\n<code>https://api.openai.com/v1</code>, y elige un modelo — usamos <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\">Salida</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\">Clave de API guardada.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">URL base [https://api.openai.com/v1]:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Seleccionar modelo predeterminado:</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=\"Eligiendo el modelo predeterminado en hermes setup model — gpt-5.5 seleccionado de la lista de OpenAI\" src=\"https://development-wec.wiline.com/docs/es/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>Usa Modelos WEC en su lugar</div><div class=\"admonitionContent_BuS1\"><p>Para ejecutar inferencias en el propio endpoint de WiLine, elige <strong>Endpoint personalizado</strong>, establece la URL base\nen <code>https://inference.wiline.com/v1</code>, pega tu clave de API de WEC y usa un modelo como\n<code>glm-5.2</code>. Mismo flujo compatible con OpenAI.</p></div></div>\n<p>Confirma que el modelo y la clave están saludables:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_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\">Salida</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\">◆ Archivos de configuración</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ✓ El archivo ~/.hermes/.env existe</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ✓ Clave de API o endpoint personalizado configurado</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\">◆ Proveedor de memoria</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ✓ Memoria incorporada activa (no se configuró proveedor externo — esto está bien)</span><br></div></code></pre></div></div>\n<p>Las líneas restantes <code>⚠</code> en <code>doctor</code> son integraciones de herramientas opcionales (búsqueda web,\nDiscord, generación de imágenes) — no necesarias para esta guía.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--chatear-y-guardar-una-memoria\">Paso 4 — Chatear y guardar una memoria<a href=\"https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/#paso-4--chatear-y-guardar-una-memoria\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — Chatear y guardar una memoria\" title=\"Enlace directo al Paso 4 — Chatear y guardar una memoria\" translate=\"no\">​</a></h2>\n<p>Inicia una sesión interactiva:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_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=\"La pantalla de bienvenida del Agente Hermes TUI — logo ASCII, herramientas y habilidades disponibles, modelo gpt-5.5, y el aviso de chat\" src=\"https://development-wec.wiline.com/docs/es/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>Envíale un hecho y pídele que lo recuerde — observa cómo se activa la herramienta <code>🧠 memory</code>:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">En el TUI de Hermes</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\">● Por favor guarda en tu memoria a largo plazo: mi nombre en clave del proyecto WEC es</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  Bluefin-7, ejecutándose en la caja de OpenClaw.</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\">    Guardado en la memoria a largo plazo: tu nombre en clave del proyecto WEC es Bluefin-7,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    ejecutándose en la caja de OpenClaw.</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 activando la herramienta de memoria y confirmando que el hecho fue guardado en la memoria a largo plazo\" src=\"https://development-wec.wiline.com/docs/es/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>Esa escritura crea <code>~/.hermes/memories/MEMORY.md</code>. Sal con <code>/exit</code> o <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>Problema al pegar</div><div class=\"admonitionContent_BuS1\"><p>El TUI envía al presionar enter, por lo que pegar texto de varias líneas puede enviar un mensaje medio escrito.\nSi un prompt solo captura un fragmento, <strong>escríbelo en una línea</strong> a mano.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5--probar-que-la-memoria-sobrevive-a-un-reinicio\">Paso 5 — Probar que la memoria sobrevive a un reinicio<a href=\"https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/#paso-5--probar-que-la-memoria-sobrevive-a-un-reinicio\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 5 — Probar que la memoria sobrevive a un reinicio\" title=\"Enlace directo al Paso 5 — Probar que la memoria sobrevive a un reinicio\" translate=\"no\">​</a></h2>\n<p>Este es el objetivo principal. La puerta de enlace de Hermes está instalada como un <strong>servicio de usuario systemd</strong>\ncon <strong>linger habilitado</strong>, por lo que vuelve automáticamente después de un reinicio — sin necesidad de iniciar sesión.\nReinicia el servicio y luego reinicia toda la caja para hacer la prueba a prueba de fallos:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_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\"># reinicia solo el servicio de puerta de enlace</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\"># luego reinicia toda la caja</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Reiniciando la puerta de enlace y reiniciando la caja — la conexión SSH se cierra mientras el host se apaga\" src=\"https://development-wec.wiline.com/docs/es/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>Reconéctate una vez que la caja esté activa y confirma que el servicio se reinició por sí mismo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_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\">Salida</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 - Puerta de enlace del Agente Hermes - Integración de Plataforma de Mensajería</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 mostrando hermes-gateway activo y en ejecución desde el arranque, iniciado automáticamente por linger después del reinicio\" src=\"https://development-wec.wiline.com/docs/es/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>Ahora abre una sesión <strong>nueva</strong> y pregunta — sin contexto previo, ID de sesión completamente nuevo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_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\">En el TUI de Hermes</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\">● ¿Cuál es mi nombre en clave del proyecto WEC, y en qué caja se ejecuta?</span><br></div><div class=\"token-line\" style=\"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\">    Tu nombre en clave del proyecto WEC es Bluefin-7, y se ejecuta en la</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    caja de OpenClaw.</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=\"Una nueva sesión de Hermes después del reinicio recordando correctamente el nombre en clave Bluefin-7 desde el disco\" src=\"https://development-wec.wiline.com/docs/es/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>Recordó el hecho desde el disco después de un reinicio completo. Inspecciona dónde vive ese estado duradero:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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\">El nombre en clave del proyecto WEC del usuario es Bluefin-7, ejecutándose en la caja de OpenClaw.</span><br></div><div class=\"token-line\" style=\"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: base de datos SQLite 3.x, ...</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>Un <code>MEMORY.md</code> en texto plano que puedes leer y editar, respaldado por un <code>state.db</code> de SQLite para\nsesiones. Esa es la memoria persistente — y acaba de sobrevivir a un reinicio.</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>Cómo se almacena la memoria — dos archivos, dos trabajos</div><div class=\"admonitionContent_BuS1\"><p>Cuando le pides a Hermes que recuerde algo, escribe el hecho en\n<strong><code>~/.hermes/memories/MEMORY.md</code></strong> — Markdown plano que puedes abrir, leer e incluso editar a mano. Por separado, tu <strong>historial completo de conversación</strong> se guarda en\n<strong><code>~/.hermes/state.db</code></strong>, una base de datos <strong>SQLite</strong>. Ambos viven en disco, por lo que un\nreinicio no los pierde.</p><p>La forma más sencilla de ver lo que sabe el agente es simplemente leer el 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>Para mirar dentro de la base de datos, ten en cuenta que el <em>motor</em> de SQLite está integrado en Python, por lo que Hermes\nlo usa sin ninguna instalación adicional. La opcional herramienta de línea de comandos <code>sqlite3</code> — para\nnavegar por la base de datos a mano — no está instalada por defecto. Si la quieres:</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=\"Salida de .tables en state.db — tablas de sesiones y mensajes junto a la familia de mensajes_fts FTS5\" src=\"https://development-wec.wiline.com/docs/es/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>Verás tablas <code>sessions</code> y <code>messages</code> que contienen la conversación, además de una familia de\ntablas <code>messages_fts*</code>. Esos son índices de texto completo <strong>FTS5</strong> de SQLite — incluyendo una\nvariante de trigramas para coincidencias difusas — que permiten a Hermes buscar sus propias conversaciones pasadas:</p><div class=\"language-sql codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Salida (de .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>Y cada sesión es una fila que puedes inspeccionar — aquí están las dos de esta guía, la\nsegunda creada <em>después</em> del reinicio:</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\">Salida</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>Nunca <em>necesitas</em> la CLI para que Hermes funcione — es solo para inspeccionar la base de datos\ntú mismo. Día a día, el <code>MEMORY.md</code> legible por humanos es todo lo que tienes que mirar.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Ahora ejecutas un <strong>agente cuya memoria sobrevive a los reinicios</strong> — y sabes exactamente dónde vive esa\nmemoria en disco.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"bonificación--leer-la-conversación-que-almacenó-el-agente\">Bonificación — leer la conversación que almacenó el agente<a href=\"https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/#bonificaci%C3%B3n--leer-la-conversaci%C3%B3n-que-almacen%C3%B3-el-agente\" class=\"hash-link\" aria-label=\"Enlace directo al Bonificación — leer la conversación que almacenó el agente\" title=\"Enlace directo al Bonificación — leer la conversación que almacenó el agente\" translate=\"no\">​</a></h2>\n<p>La tabla <code>messages</code> contiene el texto real de cada turno. Cada fila tiene un <code>role</code> —\n<code>user</code> (lo que escribiste), <code>assistant</code> (la respuesta de Hermes), o <code>tool</code> (el resultado de una herramienta) — para que\npuedas reproducir todo el intercambio:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_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\">Salida</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=\"La tabla de mensajes en state.db mostrando la conversación completa — filas de usuario, asistente y herramienta a través de ambas sesiones\" src=\"https://development-wec.wiline.com/docs/es/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>La parte interesante es <em>cómo</em> se escribió la memoria. El asistente no editó un archivo\ndirectamente — emitió una <strong>llamada a la herramienta <code>memory</code></strong>, y esa herramienta es la que escribió\n<code>MEMORY.md</code>. Puedes ver la llamada exacta registrada en el historial:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_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\">Salida</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\">\\\"El nombre en clave del proyecto WEC del usuario es Bluefin</span><span class=\"token number\" style=\"color:#36acaa\">-7</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> ejecutándose en la caja de OpenClaw.\\\"</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=\"La llamada a la herramienta de memoria registrada en la tabla de mensajes — la operación de adición que escribió el hecho de Bluefin-7 en MEMORY.md\" src=\"https://development-wec.wiline.com/docs/es/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>Así que la cadena completa es: <strong>tú escribes → el agente decide que vale la pena mantenerlo → llama a la\nherramienta <code>memory</code> con <code>action: add</code> → eso escribe el hecho en <code>MEMORY.md</code></strong>, mientras que\ntoda la conversación se mantiene en <code>state.db</code>, completamente buscable a través de FTS5. Dos almacenes, una\nmemoria duradera.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-puedes-hacer-con-ello\">¿Qué puedes hacer con ello?<a href=\"https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/#qu%C3%A9-puedes-hacer-con-ello\" class=\"hash-link\" aria-label=\"Enlace directo al ¿Qué puedes hacer con ello?\" title=\"Enlace directo al ¿Qué puedes hacer con ello?\" translate=\"no\">​</a></h2>\n<p>Una memoria que sobrevive a los reinicios solo importa por lo que el agente <em>hace</em> con ella.\nCon Hermes en funcionamiento, la misma instalación ofrece estas capacidades (las viste en la\nlista de herramientas de <code>hermes doctor</code>) — aquí está lo que desbloquean día a día:</p>\n<ul>\n<li class=\"\"><strong>Realiza trabajo real en la caja.</strong> Las herramientas <code>terminal</code> y <code>code_execution</code> le permiten actuar,\nno solo chatear — por ejemplo, <em>\"clona este repositorio, instala dependencias y ejecuta las pruebas,\"</em> o <em>\"verifica\npor qué se está llenando el disco.\"</em> Lo hace en la Instancia WEC y reporta de vuelta.</li>\n<li class=\"\"><strong>Controla desde tu teléfono.</strong> Una puerta de enlace de mensajería (Telegram, Discord, Slack, y\nmás) significa que puedes darle una tarea desde cualquier lugar — sin SSH. <em>(Configuración en la Parte 2.)</em></li>\n<li class=\"\"><strong>Ponlo en un horario.</strong> La herramienta <code>cronjob</code> ejecuta trabajos recurrentes: un <strong>informe matutino</strong>,\nun <strong>informe semanal de uso del disco</strong>, o <strong>vigila un servicio y avísame cuando falle</strong> — entregado a tu canal de mensajería. <em>(Parte 3.)</em></li>\n<li class=\"\"><strong>Navega y busca en la web.</strong> Con las herramientas de navegador y búsqueda web puede obtener información en vivo y actuar en páginas reales. <em>(Estas necesitan claves de API de proveedor — consulta <code>hermes setup tools</code>.)</em></li>\n<li class=\"\"><strong>Desarrolla habilidades reutilizables y delega.</strong> Puede construir <strong>habilidades</strong> a partir de lo que aprende y\ndelegar subtareas a <strong>subagentes</strong> — la parte \"agente que crece contigo\".</li>\n<li class=\"\"><strong>Recuerda a través de todo ello.</strong> Todo lo anterior está respaldado por la memoria persistente que\nacabas de configurar, por lo que el contexto se lleva de una tarea — y un día — al siguiente.</li>\n</ul>\n<p>El resto de esta serie convierte cada una de estas en una guía práctica (consulta <em>¿Qué sigue?</em>).</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-de-problemas-real\">Solución de problemas (real)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/#soluci%C3%B3n-de-problemas-real\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas (real)\" title=\"Enlace directo al Solución de problemas (real)\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">\n<p><strong>La Configuración Rápida (Nous Portal) se queda colgada en \"Esperando aprobación (sondeando)\".</strong> El inicio de sesión en el Portal\nnecesita una suscripción/crédito activo. Si no tienes uno, <code>Ctrl+C</code> y usa\n<em>Configuración completa</em> con tu propia clave en su lugar (Paso 3).</p>\n</li>\n<li class=\"\">\n<p><strong><code>doctor</code> muestra \"No se encontró clave de API en ~/.hermes/.env\".</strong> El modelo predeterminado apunta a\nPortal/OpenRouter. Ejecuta <code>hermes setup model</code>, elige tu proveedor y pega la\nclave — <code>doctor</code> luego informa <em>\"Clave de API o endpoint personalizado configurado.\"</em></p>\n</li>\n<li class=\"\">\n<p><strong>Advertencia de <code>.env</code> obsoleta al iniciar la puerta de enlace</strong> (por ejemplo, <code>MESSAGING_CWD</code>). Inofensiva;\nsigue la sugerencia para mover la configuración a <code>config.yaml</code> cuando sea conveniente.</p>\n</li>\n<li class=\"\">\n<p><strong>Repetidos <code>WARNING [Telegram] …</code> en los registros de la puerta de enlace.</strong> Si seguiste las partes anteriores\nde OpenClaw y agregaste un canal de Telegram, la configuración de Hermes detectó OpenClaw y\n<strong>importó esas configuraciones de mensajería</strong> en <code>~/.hermes/.env</code>. Como omitimos\nterminar la mensajería aquí, la puerta de enlace tiene un token pero no una configuración completa, por lo que sigue\nreintentando y registrando esta advertencia. (Una caja nueva sin configuración de Telegram de OpenClaw no\nlo verá.) Elimina las entradas sobrantes y reinicia:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Si ese token alguna vez fue expuesto, revócalo en <strong>@BotFather</strong> de Telegram y emite uno nuevo.</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=\"qué-sigue\">¿Qué sigue?<a href=\"https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al ¿Qué sigue?\" title=\"Enlace directo al ¿Qué sigue?\" translate=\"no\">​</a></h2>\n<p>Ahora que Hermes está funcionando con memoria duradera, el resto de la serie convierte cada\ncapacidad anterior en una guía práctica:</p>\n<ul>\n<li class=\"\"><strong>Parte 2 — Controla a Hermes desde Telegram.</strong> Termina de migrar el canal de Telegram que Hermes\nya importó de OpenClaw (<code>hermes setup gateway</code>, listas de permitidos, verifica que el bot\nresponda), para que puedas darle tareas desde tu teléfono.</li>\n<li class=\"\"><strong>Parte 3 — Ponlo en un horario.</strong> Usa la herramienta cron para un informe matutino, un informe semanal,\ny una alerta de salud del servicio entregada a tu canal.</li>\n<li class=\"\"><strong>Parte 4 — Habilidades personalizadas y subagentes.</strong> Enséñale habilidades reutilizables y delega trabajo.</li>\n</ul>\n<p>Los enlaces aparecerán aquí a medida que cada parte se publique.</p>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/",
            "title": "Autoalojar el Agente Hermes con memoria persistente",
            "summary": "Despliega el Agente Hermes de código abierto de Nous Research en tu Instancia WEC — con memoria persistente respaldada por SQLite que sobrevive a un reinicio completo. Instalación real, configuración del modelo y una prueba de memoria que sobrevive al reinicio.",
            "date_modified": "2026-06-25T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "autoalojamiento",
                "hermes",
                "agentes",
                "memoria"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/openclaw-wordmark-003352a1a7f02afc3bd877ae6f2dc175.png\" alt=\"OpenClaw\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__docker\" src=\"https://development-wec.wiline.com/docs/es/assets/images/netbird-dark-0d2ba9c783792904def84b7e687b8f95.png\" alt=\"NetBird\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 6 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->6</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->6<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting OpenClaw</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Deploy your own AI assistant</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real HTTPS + auth</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Chat from Telegram</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">4</span><span class=\"skillTracker__skill\" data-state=\"current\">Private mesh access</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/run-openclaw-on-wec-models/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run it on WEC models</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/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>En <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/\">Artículo 2</a> colocamos Caddy frente a\nOpenClaw para HTTPS, y en <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/\">Artículo 3</a>\nagregamos un canal de Telegram. La puerta de enlace funciona — pero Caddy sigue escuchando en\n<code>0.0.0.0</code>, accesible por cualquier cosa que pueda enrutar hacia la caja. Este es el proyecto final\nde la serie: unimos el servidor y tu laptop a una <strong>malla NetBird</strong>,\nredirigimos <code>openclaw.local</code> a la IP de la malla y <strong>cerramos los puertos públicos</strong>. La misma\nURL <code>https://openclaw.local/chat</code> sigue funcionando — pero solo para tus dispositivos.\nCada comando y error a continuación proviene de la ejecución real.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>Continúa desde los Artículos 1–3 en la misma Instancia WEC — <strong>Ubuntu</strong>, OpenClaw detrás de\n<strong>Caddy 2</strong>. Usamos <strong>NetBird gestionado</strong> (el nivel gratuito en\n<a href=\"https://app.netbird.io/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">app.netbird.io</a>) con el agente <strong>0.73.2</strong> tanto en el\nservidor (Linux, núcleo WireGuard) como en el cliente (macOS, espacio de usuario). NetBird es\n<a href=\"https://www.wireguard.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">WireGuard</a>-basado y de código abierto; también puedes\nautoalojar el plano de control, que es un tema más amplio para otro día.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"cómo-funciona-netbird-y-lo-que-construirás\">Cómo funciona NetBird (y lo que construirás)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#c%C3%B3mo-funciona-netbird-y-lo-que-construir%C3%A1s\" class=\"hash-link\" aria-label=\"Enlace directo al Cómo funciona NetBird (y lo que construirás)\" title=\"Enlace directo al Cómo funciona NetBird (y lo que construirás)\" translate=\"no\">​</a></h2>\n<p>NetBird tiene dos partes, y la distinción es el punto principal:</p>\n<ol>\n<li class=\"\"><strong>El plano de control</strong> (el <em>servicio de gestión</em> + <em>señal</em> gestionado de NetBird) — esta es la única cosa con una presencia pública. Cada dispositivo que quiere unirse <strong>se autentica</strong> (con tu clave de configuración o inicio de sesión SSO), y media el intercambio de la clave pública <a href=\"https://www.wireguard.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">WireGuard</a> de cada par.</li>\n<li class=\"\"><strong>El plano de datos</strong> — una vez autenticados, los pares se comunican <strong>directamente entre sí</strong>, cifrados de extremo a extremo a través de WireGuard. Tu tráfico real nunca fluye a través de un puerto público que gestionas; el plano de control solo hizo las presentaciones.</li>\n</ol>\n<p>Así que estás construyendo una malla donde <strong>Peer A</strong> (tu laptop) alcanza a <strong>Peer B</strong> (la caja OpenClaw) por su dirección privada <code>100.x</code> — y <code>openclaw.local</code> apunta allí, con Caddy vinculado solo a esa interfaz. El lado público/LAN no tiene nada escuchando.</p>\n<p>La idea clave para más adelante: <strong>la malla no está limitada a dos pares.</strong> Autentica una vez, y cada nuevo servicio — Ollama, un VDI, WordPress — se une como otro par con su propia dirección <code>100.x</code>, accesible de forma privada <strong>sin puertos públicos que abrir</strong>. Configuras el modelo de autenticación una vez; agregas servicios para siempre.</p>\n<!-- -->\n<p>El internet público solo llega a <strong>el plano de control de NetBird</strong> — y eso solo\n<em>autentica</em> pares, nunca transporta tu tráfico de aplicación. Tus servicios no están\n\"protegidos\" tanto como <strong>invisibles</strong>: OpenClaw no tiene IP pública, así que simplemente\n<strong>no hay ruta</strong> hacia él desde internet. La única forma de entrar es ser un\npar autenticado en la malla.</p>\n<p>En este tutorial conectamos los primeros dos pares — <strong>tu laptop</strong> y la\n<strong>caja OpenClaw</strong>. Los otros servicios hacen el punto: una vez que la autenticación está configurada,\n<code>Ollama</code>, un <code>VDI</code>, <code>WordPress</code> simplemente se unen a la misma malla como otro par <code>100.x</code>\n— sin puertos públicos, sin exposición por servicio.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"establecer-un-plano-de-control-de-netbird\">Establecer un plano de control de NetBird<a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#establecer-un-plano-de-control-de-netbird\" class=\"hash-link\" aria-label=\"Enlace directo al Establecer un plano de control de NetBird\" title=\"Enlace directo al Establecer un plano de control de NetBird\" translate=\"no\">​</a></h2>\n<p>Cada malla de NetBird tiene dos roles: <strong>pares</strong> — tus máquinas, es decir, la caja OpenClaw\nde las Partes 1–3 y tu laptop — y un <strong>plano de control</strong> que autentica esos\npares y emite sus claves de unión. Ya tienes el par (tu caja OpenClaw);\nahora necesitas un plano de control al que unirse. Dos formas de obtener uno:</p>\n<ul>\n<li class=\"\"><strong>Opción A — autoalojarlo en WiLine (recomendado).</strong> Despliega la <strong>plantilla de NetBird Marketplace</strong> de WEC: una <strong>Instancia WEC dedicada</strong> que ejecuta tu <em>propio</em> plano de control de NetBird — panel, certificado de confianza, cero shell. Ahora tu <em>entera</em> red privada vive en WiLine — la caja OpenClaw <strong>y</strong> su plano de control — sin dependencia de terceros.</li>\n<li class=\"\"><strong>Opción B — NetBird gestionado (sin caja extra).</strong> ¿No quieres ejecutar tu propio plano de control? Usa el nivel gratuito en <a href=\"https://app.netbird.io/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">app.netbird.io</a> — inicia sesión y listo; el plano de control es la nube de NetBird. Luego salta a <a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#now-enroll-your-machines\" class=\"\">Inscribir tus máquinas</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>Dos cajas, a propósito — y tu caja OpenClaw está intacta</div><div class=\"admonitionContent_BuS1\"><p>Con <strong>la Opción A</strong> ejecutarás <strong>dos instancias</strong>: tu <strong>caja OpenClaw</strong> (Partes 1–3,\nsin cambios) y una <strong>segunda caja de plano de control dedicada</strong> (esta plantilla). Esa es la\ntopología correcta — un plano de control debe ser su propia máquina, nunca compartiendo con una\ncarga de trabajo. Con <strong>la Opción B</strong> solo hay tu una caja OpenClaw. <strong>De cualquier manera nunca\nreconstruyes la caja OpenClaw</strong> — en <a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#now-enroll-your-machines\" class=\"\">Inscribir tus máquinas</a>\nsolo instalas el agente de NetBird en ella y se une a la malla como un par.</p></div></div>\n<p>Los pasos a continuación configuran <strong>la Opción A</strong>. WEC ejecuta la instalación; le das un correo electrónico y una\nIP pública, y unos minutos después tienes tu propio panel de NetBird en\n<code>https://&lt;your-ip&gt;.apps.wiline.cloud</code> con un certificado válido.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"1-obtener-una-ip-pública-elástica\">1. Obtener una IP pública (Elástica)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#1-obtener-una-ip-p%C3%BAblica-el%C3%A1stica\" class=\"hash-link\" aria-label=\"Enlace directo al 1. Obtener una IP pública (Elástica)\" title=\"Enlace directo al 1. Obtener una IP pública (Elástica)\" translate=\"no\">​</a></h3>\n<p>El panel necesita una dirección pública. Bajo <strong>Redes → IPs Elásticas</strong>, asigna una\nnueva IP o elige una libre, y anota su valor (la nuestra: <code>108.60.112.165</code>). Debe estar\nen la <strong>VPC y zona en la que desplegarás</strong>. Pasos completos:\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/cloud_portal/platform/networks/elastic_ip/\">Configurar IPs Elásticas</a>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Una IP Elástica disponible en Redes → IPs Elásticas\" src=\"https://development-wec.wiline.com/docs/es/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-desplegar-una-vm-con-la-plantilla\">2. Desplegar una VM con la plantilla<a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#2-desplegar-una-vm-con-la-plantilla\" class=\"hash-link\" aria-label=\"Enlace directo al 2. Desplegar una VM con la plantilla\" title=\"Enlace directo al 2. Desplegar una VM con la plantilla\" translate=\"no\">​</a></h3>\n<p>Esta nueva instancia se convierte en tu <strong>plano de control de NetBird</strong> — una caja separada de tu\ninstancia OpenClaw, que permanece intacta. Abre <strong>Desplegar → Desplegar una VM</strong> (guía completa:\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/cloud_portal/platform/compute/instances/compute_instance/\">Desplegar una Máquina Virtual</a>).\nLas elecciones que importan aquí:</p>\n<ul>\n<li class=\"\"><strong>Red</strong> — elige la red en la <strong>misma VPC/zona que tu IP Elástica</strong> (usamos <code>Subred Local</code> en <code>Princeton-VPC</code>), para que la IP pueda adjuntarse después.</li>\n<li class=\"\"><strong>Plantilla → Marketplace → Ubuntu 24.04 LTS Netbird.</strong></li>\n<li class=\"\"><strong>Configuración de Plantilla</strong> — tu <strong>correo electrónico de Let's Encrypt</strong> y la <strong>IP Elástica del paso 1</strong> como la dirección externa.</li>\n</ul>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Configuración de Plantilla — correo electrónico de Let&amp;#39;s Encrypt y la IP Elástica\" src=\"https://development-wec.wiline.com/docs/es/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>Termina el asistente y despliega.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"3-adjuntar-la-ip-elástica-a-la-nueva-instancia\">3. Adjuntar la IP Elástica a la nueva instancia<a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#3-adjuntar-la-ip-el%C3%A1stica-a-la-nueva-instancia\" class=\"hash-link\" aria-label=\"Enlace directo al 3. Adjuntar la IP Elástica a la nueva instancia\" title=\"Enlace directo al 3. Adjuntar la IP Elástica a la nueva instancia\" translate=\"no\">​</a></h3>\n<p>La IP no está vinculada en el momento del despliegue — la adjuntas una vez que la VM esté activa. Ve a\n<strong>Redes → IPs Elásticas → tu IP → ⋯ → Asignar</strong>, y selecciona tu nueva instancia\n(<code>netbird-tutorial</code>). Solo las VMs en la <strong>misma VPC</strong> aparecen en la lista.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Asignando la IP Elástica a la nueva VM\" src=\"https://development-wec.wiline.com/docs/es/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-espera-el-primer-arranque-luego-verifica-que-esté-activa\">4. Espera el primer arranque, luego verifica que esté activa<a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#4-espera-el-primer-arranque-luego-verifica-que-est%C3%A9-activa\" class=\"hash-link\" aria-label=\"Enlace directo al 4. Espera el primer arranque, luego verifica que esté activa\" title=\"Enlace directo al 4. Espera el primer arranque, luego verifica que esté activa\" translate=\"no\">​</a></h3>\n<p>La instancia muestra <strong>Ejecutándose</strong> en <strong>Cómputo → Instancias</strong> en un minuto — pero la\nplantilla sigue trabajando durante unos minutos después de eso, descargando e iniciando los\ncontenedores de NetBird y levantando el panel. No esperes que la URL responda\ninmediatamente.</p>\n<p>Verifica desde tu <strong>laptop</strong> a través de HTTPS (no <code>ping</code> — ICMP está bloqueado, así que un\nping fallido no significa que esté caída):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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> / conexión rechazada → aún en provisión; espera un minuto y vuelve a intentar.</li>\n<li class=\"\"><code>301</code> luego <code>200</code> → está activa (HTTP redirige a HTTPS).</li>\n</ul>\n<p>Para verlo directamente, accede por SSH (<code>ssh ubuntu@&lt;your-ip&gt;</code>) y confirma que los contenedores están\nejecutándose y los puertos están vinculados:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_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>Está listo una vez que <strong><code>netbird-dashboard</code>, <code>netbird-traefik</code>, y <code>netbird-server</code></strong> muestran\n<strong>Arriba</strong> y <strong>80/443</strong> están escuchando.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"5-abre-el-panel-y-crea-tu-cuenta\">5. Abre el panel y crea tu cuenta<a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#5-abre-el-panel-y-crea-tu-cuenta\" class=\"hash-link\" aria-label=\"Enlace directo al 5. Abre el panel y crea tu cuenta\" title=\"Enlace directo al 5. Abre el panel y crea tu cuenta\" translate=\"no\">​</a></h3>\n<p>Abre <strong><code>https://&lt;your-ip&gt;.apps.wiline.cloud</code></strong> — haz clic en la advertencia del certificado\nsi recibes una (el mismo clic de paso de certificado autofirmado que en\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/\">Artículo 2</a>).</p>\n<p>Este es <strong>tu propio</strong> plano de control de NetBird, ejecutándose en tu Instancia WEC. En la\nprimera visita te pide que <strong>crees la cuenta de administrador</strong> — ingresa un <strong>nombre, correo\nelectrónico y contraseña</strong>, haz clic en <strong>Crear</strong>, luego <strong>inicia sesión</strong> con esas mismas credenciales.\n(Esa pantalla de configuración aparece solo una vez; después el panel muestra una página normal de\n<strong>Iniciar sesión</strong>.)</p>\n<p>Llegas al panel de NetBird — tu plano de control privado en tu propio dominio:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El panel de NetBird autoalojado\" src=\"https://development-wec.wiline.com/docs/es/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>Haz clic en <strong>Agregar Par</strong> y NetBird te entrega el comando de inscripción para <em>tu</em> servidor —\nnota el <code>--management-url</code> apuntando a tu propio dominio:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Agregar Par en el panel autoalojado — nota la bandera --management-url\" src=\"https://development-wec.wiline.com/docs/es/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>(Ese segundo comando abre un inicio de sesión SSO en el navegador. Para un servidor <strong>sin cabeza</strong>,\nagrega <code>--setup-key &lt;KEY&gt;</code> desde la página de <strong>Claves de Configuración</strong> de tu panel en su lugar — igual que el flujo gestionado, solo que con <code>--management-url</code> agregado.)</p>\n<p>Este es el panel donde creas las claves de configuración que inscriben máquinas — que es exactamente lo que hacemos a continuación.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"ahora-inscribe-tus-máquinas\">Ahora inscribe tus máquinas<a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#ahora-inscribe-tus-m%C3%A1quinas\" class=\"hash-link\" aria-label=\"Enlace directo al Ahora inscribe tus máquinas\" title=\"Enlace directo al Ahora inscribe tus máquinas\" translate=\"no\">​</a></h2>\n<p>Tienes un plano de control (tu propio panel de plantilla, o NetBird gestionado). Ahora coloca\nlas dos máquinas en la malla — la <strong>caja OpenClaw</strong> de las Partes 1–3 y tu\n<strong>laptop</strong> — y luego cierra los puertos públicos de la caja. Estos pasos son los mismos sin importar qué plano de control elegiste.</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>De dónde proviene tu clave de configuración</div><div class=\"admonitionContent_BuS1\"><p>Los comandos a continuación utilizan <strong>NetBird gestionado</strong> (<code>app.netbird.io</code>) como ejemplo. Si\ndesplegaste la <strong>plantilla de WEC</strong> en su lugar, es idéntico excepto: crea la clave de configuración\nen <strong>tu propio</strong> panel (<code>https://&lt;your-ip&gt;.apps.wiline.cloud</code>) y agrega\n<strong><code>--management-url https://&lt;your-ip&gt;.apps.wiline.cloud</code></strong> a <code>netbird up</code>, para que el\nagente apunte a <em>tu</em> servidor en lugar de a la nube gestionada.</p></div></div>\n<p><strong>Requisitos previos:</strong> OpenClaw ejecutándose detrás de Caddy de\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/\">Artículos 1–3</a> (accesible en\n<code>https://openclaw.local</code>), un plano de control del paso anterior, y acceso a la shell en\nla caja más administrador en tu laptop.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--instalar-el-agente-de-netbird-en-la-caja-openclaw\">Paso 1 — Instalar el agente de NetBird en la caja OpenClaw<a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#paso-1--instalar-el-agente-de-netbird-en-la-caja-openclaw\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Instalar el agente de NetBird en la caja OpenClaw\" title=\"Enlace directo al Paso 1 — Instalar el agente de NetBird en la caja OpenClaw\" translate=\"no\">​</a></h2>\n<p>Esta es la caja de las Partes 1–3 (una Instancia WEC) — la estamos inscribiendo como un <strong>par</strong>,\nque es separado de cualquier servidor de plano de control. NetBird envía un instalador de una línea:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Problema (real):</strong> en Ubuntu la instalación activa el diálogo <code>needrestart</code> —\nuna caja azul <em>\"¿Qué servicios deberían reiniciarse?\"</em>. <code>Tab</code> entre botones puede ser\nabsorbido por algunas configuraciones de SSH/terminal; usa <strong>las teclas de flecha</strong> para moverte,\n<strong>Espacio</strong> para alternar, <strong>Enter</strong> para confirmar (<code>&lt;Ok&gt;</code> es el predeterminado). El paquete ya está\nen su lugar para cuando aparece esta caja, así que incluso si sales con <code>Ctrl+C</code>, el agente\nestá instalado — volver a ejecutar el instalador solo te lo dirá:</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 parece estar ya instalado, por favor elimínalo antes de continuar</span><br></div></code></pre></div></div>\n<p>(Si vuelves a ejecutar el instalador después de este tutorial, verás en su lugar\n<code>El servicio de NetBird está en ejecución, por favor deténlo antes de continuar</code> — el agente ya está\nactivo, así que no hay nada que reinstalar.)</p>\n<p>Confirma la versión:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_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=\"paso-2--crear-una-clave-de-configuración-y-unirse-a-la-caja-openclaw\">Paso 2 — Crear una clave de configuración y unirse a la caja OpenClaw<a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#paso-2--crear-una-clave-de-configuraci%C3%B3n-y-unirse-a-la-caja-openclaw\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — Crear una clave de configuración y unirse a la caja OpenClaw\" title=\"Enlace directo al Paso 2 — Crear una clave de configuración y unirse a la caja OpenClaw\" translate=\"no\">​</a></h2>\n<p>Una <strong>clave de configuración</strong> es el token que inscribe un dispositivo en tu malla — ideal para un\nservidor sin cabeza (sin necesidad de SSO en el navegador).</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>¿Primera vez en el panel? Salta los desvíos</div><div class=\"admonitionContent_BuS1\"><p>Algunas cosas intentarán enviarte en la dirección equivocada en una cuenta nueva — aquí está la\nlínea directa:</p><ul>\n<li class=\"\"><strong>¿Sin cuenta aún?</strong> Crea una primero (inicia sesión con Google/GitHub/correo electrónico). No puedes generar una clave hasta que estés dentro.</li>\n<li class=\"\"><strong>El asistente de incorporación</strong> (\"Agrega tu primer recurso\" — <em>IP única / Subred completa / Dominio</em>) aparece de inmediato. <strong>Sáltalo.</strong> Los recursos son para exponer subredes/dominios; solo necesitamos dos pares simples, así que no es necesario.</li>\n<li class=\"\"><strong>El botón naranja \"Agregar Par\"</strong> abre el <strong>flujo de instalación SSO</strong> (descargar la aplicación, registrarse con correo electrónico, conectarse desde la bandeja). Eso está destinado a un escritorio con un navegador — <strong>no</strong> a un servidor sin cabeza. No lo sigas.</li>\n<li class=\"\">En su lugar, ve directamente a la página de <strong>Claves de Configuración</strong>: <strong><code>https://app.netbird.io/setup-keys</code></strong>.</li>\n</ul></div></div>\n<ol>\n<li class=\"\">Abre <strong><code>https://app.netbird.io/setup-keys</code></strong>.</li>\n<li class=\"\"><strong>Crear Clave de Configuración</strong> → nombre <code>openclaw-tutorial</code>, alterna <strong>Reutilizable</strong> (para que funcione también para la laptop), por defecto de otro modo.</li>\n<li class=\"\">Copia la clave.</li>\n</ol>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Creando una clave de configuración reutilizable en el panel de NetBird\" src=\"https://development-wec.wiline.com/docs/es/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>Conecta la VM (reemplaza con tu clave):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Trata la clave de configuración como un secreto</div><div class=\"admonitionContent_BuS1\"><p>Cualquiera con una clave reutilizable puede agregar dispositivos a tu red. No la pegues en\nchats o commits, y <strong>elimínala</strong> del panel una vez que tus dispositivos estén\ninscritos (los pares existentes permanecen conectados).</p></div></div>\n<p>Verifica la conexión:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_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=\"estado de netbird en la VM — Gestión y Señal Conectadas, IP de malla 100.87.239.229, conteo de Pares 0/0\" src=\"https://development-wec.wiline.com/docs/es/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>Esa <strong><code>IP de NetBird</code> (<code>100.87.239.229</code>)</strong> es la dirección a la que apunta todo lo que sigue.\n<code>Conteo de Pares: 0/0</code> es esperado — nada más se ha unido aún.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--unir-tu-laptop\">Paso 3 — Unir tu laptop<a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#paso-3--unir-tu-laptop\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — Unir tu laptop\" title=\"Enlace directo al Paso 3 — Unir tu laptop\" translate=\"no\">​</a></h2>\n<p>Instala el cliente de NetBird en lo que estés usando, luego conéctate con la\n<strong>misma clave de configuración reutilizable</strong>. El comando de conexión (<code>netbird up --setup-key …</code>) es\nidéntico en cada SO — solo la instalación difiere.</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>Lo ejecutamos en macOS</div><div class=\"admonitionContent_BuS1\"><p>La salida capturada a continuación es de un Mac. Las pestañas de Windows y Linux dan la\ninstalación equivalente; los pasos de <code>netbird up</code>, <code>netbird status</code>, y ping son los\nmismos en todas partes (Windows usa <code>ping -n</code> en lugar de <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>Instala a través de Homebrew, luego inicia el servicio y conéctate:</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>Puede que veas una advertencia de gRPC única mientras el socket del daemon se activa un momento después\nde que el cliente lo llama — termina en <code>Conectado</code>, así que es inofensivo:</p><div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Salida</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>Descarga y ejecuta el <strong>instalador de NetBird para Windows</strong> desde\n<a href=\"https://app.netbird.io/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">app.netbird.io</a> (Agregar Par → Windows) o\n<a href=\"https://pkgs.netbird.io/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">pkgs.netbird.io</a>. Instala una aplicación en la bandeja del sistema y el\nCLI <code>netbird</code>. Luego, en <strong>PowerShell como Administrador</strong>, conéctate con tu clave:</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>(También puedes hacer clic en <strong>Conectar</strong> desde el icono de la bandeja y autenticarte a través de SSO, pero\nla clave de configuración es el mismo flujo de un solo uso que en las otras plataformas.)</p></div><div role=\"tabpanel\" class=\"tabItem_yrzq\" hidden=\"\"><p>El mismo instalador de una línea que la VM, luego conéctate:</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>Verifica que los dos nodos se vean entre sí, y que el túnel transporte tráfico:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_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=\"Estado de netbird en Mac mostrando Conteo de Pares 1/1 Conectado, además de un ping exitoso a la IP de malla de la VM\" src=\"https://development-wec.wiline.com/docs/es/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>Conteo de Pares: 1/1</code> — ese par es la VM. En el panel, ambas máquinas ahora\naparecen bajo <strong>Pares</strong> (porque las inscribimos con una clave de configuración en lugar de un\ninicio de sesión SSO, aparecen bajo <strong>Servidores</strong> en lugar de <strong>Dispositivos de Usuario</strong>):</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"La VM y la laptop conectadas como pares en el panel de NetBird\" src=\"https://development-wec.wiline.com/docs/es/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>¿Por qué ~200 ms?</div><div class=\"admonitionContent_BuS1\"><p>Este camino está pasando por un <strong>relay</strong> de NetBird (nota <code>Tipo de Interfaz: Espacio de Usuario</code>\nen el Mac). Está bien para una interfaz web, y NetBird a menudo actualizará a un enlace directo\nde par a par cuando las redes lo permitan. La latencia a un par retransmitido no es una\nbandera roja.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--redirigir-openclawlocal-a-la-ip-de-la-malla\">Paso 4 — Redirigir <code>openclaw.local</code> a la IP de la malla<a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#paso-4--redirigir-openclawlocal-a-la-ip-de-la-malla\" class=\"hash-link\" aria-label=\"Enlace directo al paso-4--redirigir-openclawlocal-a-la-ip-de-la-malla\" title=\"Enlace directo al paso-4--redirigir-openclawlocal-a-la-ip-de-la-malla\" translate=\"no\">​</a></h2>\n<p>Desde <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/\">Artículo 2</a>, tu archivo de hosts mapea\n<code>openclaw.local</code> a la IP LAN de la VM (<code>10.80.4.212</code>). Cámbiala por la IP de la malla\n(<code>100.87.239.229</code>). La ruta del archivo y el comando de edición difieren según el SO:</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> en macOS necesita el vacío <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> toma <code>-i</code> sin argumento:</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>El archivo de hosts está en <code>C:\\Windows\\System32\\drivers\\etc\\hosts</code> y necesita admin.\nEn <strong>PowerShell como Administrador</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>(O abre ese archivo en <strong>Notepad ejecutado como Administrador</strong> y cambia la IP a mano.)</p></div></div></div>\n<p>Prueba a través de la malla — <em>antes</em> de tocar ningún puerto:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">Salida</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> a través de la IP de la malla — el chat ahora fluye a través de NetBird. Carga\n<code>https://openclaw.local/chat?session=main</code> en el navegador para confirmar que se comporta\nexactamente como antes. El certificado TLS sigue coincidiendo porque el nombre de host no ha cambiado;\nsolo la IP detrás de él se movió.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El chat de OpenClaw cargando a través de la malla en https://openclaw.local\" src=\"https://development-wec.wiline.com/docs/es/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=\"paso-5--cerrar-los-puertos-públicos\">Paso 5 — Cerrar los puertos públicos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#paso-5--cerrar-los-puertos-p%C3%BAblicos\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 5 — Cerrar los puertos públicos\" title=\"Enlace directo al Paso 5 — Cerrar los puertos públicos\" translate=\"no\">​</a></h2>\n<p>Aquí está el estado que estamos corrigiendo. En la 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\">Salida — escuchando en 0.0.0.0 (todas las interfaces)</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 escucha en <strong><code>0.0.0.0</code></strong> — todas las interfaces, incluyendo la LAN (y cualquier NAT\ndelante de ella).</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>La trampa de ufw</div><div class=\"admonitionContent_BuS1\"><p>Tu primer instinto es <code>ufw deny 443</code>. <strong>No funcionará.</strong> Los puertos de Caddy son\npublicados por <strong>Docker</strong>, que escribe sus propias reglas de iptables que evitan la\ncadena <code>INPUT</code> de ufw. Agregarías reglas de ufw, te sentirías seguro, y 443 seguiría abierto. En\nesta caja <code>ufw</code> estaba incluso <code>inactivo</code> — y habilitarlo no habría cambiado nada\npara los puertos publicados por Docker.</p></div></div>\n<p>La solución confiable es <strong>vincular los puertos publicados a la IP de la malla</strong> en lugar de\n<code>0.0.0.0</code>, para que Caddy solo escuche en <code>wt0</code>. Edita el servicio de Caddy en\n<code>docker-compose.override.yml</code> (la sobreescritura del Artículo 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>Cambia el bloque de puertos de:</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>a vincular a la IP de la malla:</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>Recrea solo Caddy y vuelve a verificar:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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 después del cambio — Caddy ahora escuchando solo en la IP de la malla 100.87.239.229, ya no en 0.0.0.0\" src=\"https://development-wec.wiline.com/docs/es/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>Los puertos han desaparecido de <code>0.0.0.0</code> — ahora viven solo en la interfaz de malla.</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>Orden de inicio</div><div class=\"admonitionContent_BuS1\"><p>Debido a que el enlace apunta a la IP de la malla, <strong>NetBird debe estar activo antes de que Caddy inicie</strong>\n(por ejemplo, después de un reinicio, <code>wt0</code> necesita su IP antes de que Docker se vincule a ella). La\npolítica <code>restart: unless-stopped</code> ya cubre esto — Caddy reintenta hasta que\nla interfaz exista — pero vale la pena saberlo si alguna vez ves a Caddy en un bucle de\ncrash justo después del arranque.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-6--demostrar-el-bloqueo\">Paso 6 — Demostrar el bloqueo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#paso-6--demostrar-el-bloqueo\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 6 — Demostrar el bloqueo\" title=\"Enlace directo al Paso 6 — Demostrar el bloqueo\" translate=\"no\">​</a></h2>\n<p>Ambas direcciones, de la ejecución real.</p>\n<p><strong>Aún accesible a través de la malla</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>Ya no accesible en la LAN</strong> (VM, golpeando su propia IP LAN):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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=\"En la VM, curl a la antigua IP LAN ahora devuelve Conexión rechazada\" src=\"https://development-wec.wiline.com/docs/es/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>Ese es el resultado: <strong>la misma URL, accesible solo a través de tu malla, rechazada\nen todas partes.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Ahora puedes <strong>sacar cualquier servicio autoalojado de internet público</strong> sin romper cómo lo\nusas.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"limpieza\">Limpieza<a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#limpieza\" class=\"hash-link\" aria-label=\"Enlace directo al Limpieza\" title=\"Enlace directo al Limpieza\" translate=\"no\">​</a></h2>\n<p>Elimina la clave de configuración reutilizable ahora que ambos dispositivos están inscritos —\n<strong>Claves de Configuración → eliminar</strong> en el panel. Tus pares permanecen conectados; solo\nestás cerrando la puerta que usaste para agregarlos. Para eliminar un dispositivo más tarde, elimínalo bajo\n<strong>Pares → Dispositivos de Usuario</strong> y ejecuta <code>netbird down</code> en esa máquina.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-de-problemas-real\">Solución de problemas (real)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#soluci%C3%B3n-de-problemas-real\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas (real)\" title=\"Enlace directo al Solución de problemas (real)\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong>El diálogo <code>needrestart</code> no acepta <code>Tab</code>.</strong> Usa teclas de flecha / Espacio / Enter. El paquete ya está instalado para cuando aparece la caja.</li>\n<li class=\"\"><strong>Advertencia de gRPC <code>netbird.sock</code> en el primer <code>up</code>.</strong> El socket del daemon se retrasa un momento respecto al cliente. Si termina en <code>Conectado</code>, ignórala.</li>\n<li class=\"\"><strong>Las reglas de <code>ufw</code> no bloquean la puerta de enlace.</strong> Los puertos publicados por Docker evitan <code>ufw</code> — vincula el puerto a una IP específica (como arriba) o usa la propia cadena <code>DOCKER-USER</code> de Docker.</li>\n<li class=\"\"><strong>Caddy en bucle de crash después de reiniciar.</strong> Se inició antes de que <code>wt0</code> tuviera la IP de la malla. <code>restart: unless-stopped</code> lo recupera; o ordena Docker después de <code>netbird.service</code>.</li>\n<li class=\"\"><strong>Ping alto a un par.</strong> Estás retransmitido, no directo — funcional, solo más lento. Verifica <code>Tipo de Interfaz</code> y el detalle del par de NetBird para el tipo de conexió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>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=\"qué-sigue\">¿Qué sigue?<a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al ¿Qué sigue?\" title=\"Enlace directo al ¿Qué sigue?\" translate=\"no\">​</a></h2>\n<p>Eso cierra la serie de <strong>Autoalojamiento de OpenClaw</strong>:</p>\n<ol>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/\">Desplegar OpenClaw en una Instancia WEC a través de Docker Compose</a></li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/\">Asegurar OpenClaw con un proxy inverso Caddy + HTTPS</a></li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/\">Agregar un canal de Telegram a OpenClaw</a></li>\n<li class=\"\"><strong>Haz que OpenClaw sea privado con una VPN de malla NetBird</strong> ← estás aquí</li>\n</ol>\n<p>Ahora tienes un agente autoalojado que está cifrado en tránsito, emparejado con tus\ndispositivos, accesible desde tu teléfono, e invisible para el internet público — todo en\nuna pequeña Instancia WEC.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"continúa-el-viaje\">Continúa el viaje<a href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/#contin%C3%BAa-el-viaje\" class=\"hash-link\" aria-label=\"Enlace directo al Continúa el viaje\" title=\"Enlace directo al Continúa el viaje\" translate=\"no\">​</a></h3>\n<p>¿Listo para un agente que <em>recuerda</em>? Comienza la serie de <strong>Autoalojamiento de Hermes</strong> —\n<strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/\">Autoalojar el Agente Hermes con memoria persistente</a></strong> —\ndesplegado en esta misma Instancia WEC, con contexto que sobrevive a los reinicios.</p>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/",
            "title": "Haz que OpenClaw sea privado con una VPN de malla NetBird",
            "summary": "Coloca OpenClaw en una malla privada con NetBird, redirige tu nombre de host a la IP de la malla y cierra los puertos públicos — así la misma URL de chat funciona solo para tus dispositivos. Comandos reales y problemas de una ejecución en vivo.",
            "date_modified": "2026-06-24T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "autoalojamiento",
                "openclaw",
                "netbird",
                "vpn",
                "malla",
                "seguridad"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/openclaw-wordmark-003352a1a7f02afc3bd877ae6f2dc175.png\" alt=\"OpenClaw\"><span class=\"tutorialHero__plus\">+</span><svg viewBox=\"0 0 24 24\" fill=\"#26A5E4\" aria-label=\"Telegram\" class=\"tutorialHero__docker\"><path d=\"M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 6 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->6</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->6<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting OpenClaw</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Deploy your own AI assistant</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real HTTPS + auth</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">3</span><span class=\"skillTracker__skill\" data-state=\"current\">Chat from Telegram</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Private mesh access</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/run-openclaw-on-wec-models/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run it on WEC models</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/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>Has <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/\">desplegado OpenClaw</a> y\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/\">lo has asegurado detrás de HTTPS</a>. Ahora hazlo\n<em>utilizable</em> — habla con tu agente desde tu teléfono a través de Telegram. Creamos un bot,\nlo conectamos, limpiamos la puerta de emparejamiento de OpenClaw y obtenemos una respuesta real. Cada comando y\ndetalle a continuación proviene de una ejecución real.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>Continúa desde las Partes 1–2 en la misma Instancia WEC — OpenClaw <strong>2026.6.8</strong>. Telegram\nusa <strong>polling</strong> (la puerta de enlace consulta la API de Telegram), por lo que no se requiere un webhook o dominio público — esto funciona en una caja privada.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-construirás\">Lo que construirás<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/#lo-que-construir%C3%A1s\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que construirás\" title=\"Enlace directo al Lo que construirás\" translate=\"no\">​</a></h2>\n<p>Tu teléfono habla con un bot de Telegram; la puerta de enlace de OpenClaw consulta Telegram para\nmensajes, los procesa a través de tu modelo y responde — no se necesitan puertos de entrada.</p>\n<!-- -->\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/\">Parte 1</a></strong> + <strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/\">Parte 2</a></strong> completadas — OpenClaw ejecutándose en <code>~/openclaw</code></li>\n<li class=\"\">Una <strong>cuenta de Telegram</strong> y un <strong>cliente de Telegram conectado</strong> — la aplicación de teléfono/escritorio, o <code>web.telegram.org</code>. (No puedes crear un bot desde la página web pública <code>t.me</code>; consulta <a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/#troubleshooting-real-errors\" class=\"\">Solución de problemas</a>.)</li>\n</ul>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--crear-un-bot-de-telegram\">Paso 1 — Crear un bot de Telegram<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/#paso-1--crear-un-bot-de-telegram\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Crear un bot de Telegram\" title=\"Enlace directo al Paso 1 — Crear un bot de Telegram\" translate=\"no\">​</a></h2>\n<p>En la aplicación de Telegram, toca <strong>buscar</strong> y escribe <strong><code>BotFather</code></strong>, luego abre la cuenta oficial — la que tiene el <strong>check azul</strong>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Buscando a BotFather en Telegram\" src=\"https://development-wec.wiline.com/docs/es/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>Toca <strong>Iniciar</strong> — BotFather responde con los comandos que entiende (verás\nopciones como <code>/newbot</code>, <code>/mybots</code>, <code>/token</code>, y más). Envía <strong><code>/newbot</code></strong>, luego\nsigue sus dos indicaciones:</p>\n<ol>\n<li class=\"\"><strong>Nombre</strong> — un nombre para mostrar, por ejemplo, <code>Mi Agente OpenClaw</code></li>\n<li class=\"\"><strong>Nombre de usuario</strong> — debe ser único y terminar en <code>bot</code>, por ejemplo, <code>wiline_openclaw_bot</code></li>\n</ol>\n<p>BotFather confirma con <em>\"¡Listo! Felicitaciones por tu nuevo bot\"</em> y te da el\n<strong>token</strong> — la línea después de <code>Usa este token para acceder a la API HTTP:</code>, como\n<code>1234567890:AA...</code>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"BotFather emitiendo el token del bot\" src=\"https://development-wec.wiline.com/docs/es/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>El token del bot es una credencial</div><div class=\"admonitionContent_BuS1\"><p>Cualquiera que lo tenga controla tu bot. <strong>Difumínalo en la captura de pantalla anterior</strong>, y <code>/revoke</code>\nen BotFather si alguna vez se filtra.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-2--conectar-el-bot-a-openclaw\">Paso 2 — Conectar el bot a OpenClaw<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/#paso-2--conectar-el-bot-a-openclaw\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — Conectar el bot a OpenClaw\" title=\"Enlace directo al Paso 2 — Conectar el bot a OpenClaw\" translate=\"no\">​</a></h2>\n<p>En la <strong>VM</strong>, en <code>~/openclaw</code>, registra el canal con tu 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;tu-token-de-bot&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\"># Cuenta de Telegram \"default\" añadida.</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"añadiendo el canal con éxito\" 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=\"paso-3--activar-y-verificar\">Paso 3 — Activar y verificar<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/#paso-3--activar-y-verificar\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — Activar y verificar\" title=\"Enlace directo al Paso 3 — Activar y verificar\" translate=\"no\">​</a></h2>\n<p>Reinicia la puerta de enlace para que reconozca el canal, luego verifica el estado:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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: habilitado, configurado, en ejecución, modo:polling, token:config</span><br></div></code></pre></div></div>\n<p><code>modo:polling</code> significa que la puerta de enlace está consultando activamente Telegram — no se requiere puerto de entrada o webhook.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"estado de los canales mostrando Telegram funcionando en modo polling\" src=\"https://development-wec.wiline.com/docs/es/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=\"paso-4--mensaje-al-bot-puerta-de-emparejamiento\">Paso 4 — Mensaje al bot (puerta de emparejamiento)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/#paso-4--mensaje-al-bot-puerta-de-emparejamiento\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — Mensaje al bot (puerta de emparejamiento)\" title=\"Enlace directo al Paso 4 — Mensaje al bot (puerta de emparejamiento)\" translate=\"no\">​</a></h2>\n<p>Abre tu bot (<code>t.me/&lt;tu-nombre-de-bot&gt;</code>), toca <strong>Iniciar</strong>, y envía un mensaje.</p>\n<p>OpenClaw trata a los remitentes desconocidos como no confiables — así que en lugar de responder, el bot\ndevuelve un <strong>código de emparejamiento</strong> y tu id de usuario de Telegram. Este es el mismo patrón de seguridad\nque el emparejamiento de dispositivos en la Parte 2.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Bot respondiendo con el código de emparejamiento\" src=\"https://development-wec.wiline.com/docs/es/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=\"paso-5--aprobar-el-emparejamiento\">Paso 5 — Aprobar el emparejamiento<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/#paso-5--aprobar-el-emparejamiento\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 5 — Aprobar el emparejamiento\" title=\"Enlace directo al Paso 5 — Aprobar el emparejamiento\" translate=\"no\">​</a></h2>\n<p>En la <strong>VM</strong>, aprueba el código del mensaje del bot:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">código-de-emparejamiento</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\"># Remitente de telegram aprobado &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\"># Propietario del comando configurado telegram:&lt;id&gt; (commands.ownerAllowFrom estaba vacío).</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>nota</div><div class=\"admonitionContent_BuS1\"><p>Aprobar al primer usuario como <strong>propietario del comando</strong> cambia la configuración que requiere un\nreinicio de la puerta de enlace — OpenClaw lo hace <strong>automáticamente</strong> (verás\n<code>cambio de configuración requiere reinicio de la puerta de enlace</code> luego el reinicio del proveedor de Telegram en los\nregistros). Espera unos segundos para que vuelva antes de enviar otro mensaje.</p></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Aprobando el emparejamiento en la VM — &amp;quot;Remitente de telegram aprobado …&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=\"paso-6--chatea-con-tu-agente\">Paso 6 — Chatea con tu agente<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/#paso-6--chatea-con-tu-agente\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 6 — Chatea con tu agente\" title=\"Enlace directo al Paso 6 — Chatea con tu agente\" translate=\"no\">​</a></h2>\n<p>De vuelta en Telegram, simplemente habla con él — no se necesita configuración adicional; OpenClaw con tu modelo\nresponde de manera conversacional desde el principio. Prueba algo como:</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\">En una oración, ¿cuál es el objetivo de poner un proxy inverso frente a mi agente?</span><br></div></code></pre></div></div>\n<p>Responde con una respuesta real — la cadena completa está activa: tu teléfono → Telegram →\npuerta de enlace → OpenAI → respuesta. Tu agente autoalojado ahora está en tu bolsillo. (Proporcionarle\nherramientas y habilidades vendrá en guías posteriores; el chat conversacional funciona de inmediato.)</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El agente respondiendo de manera conversacional a través de Telegram\" src=\"https://development-wec.wiline.com/docs/es/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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Tu agente ahora está <strong>en tu bolsillo</strong> — una línea privada de Telegram a tu propia IA, sin necesidad de un webhook público.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-de-problemas-errores-reales\">Solución de problemas (errores reales)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/#soluci%C3%B3n-de-problemas-errores-reales\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas (errores reales)\" title=\"Enlace directo al Solución de problemas (errores reales)\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"1-botfather-iniciar-no-hace-nada-en-un-navegador\">1. BotFather \"Iniciar\" no hace nada en un navegador<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/#1-botfather-iniciar-no-hace-nada-en-un-navegador\" class=\"hash-link\" aria-label=\"Enlace directo al 1. BotFather &quot;Iniciar&quot; no hace nada en un navegador\" title=\"Enlace directo al 1. BotFather &quot;Iniciar&quot; no hace nada en un navegador\" translate=\"no\">​</a></h3>\n<p>Abrir <code>t.me/BotFather</code> en un navegador web simple muestra un botón de <strong>Iniciar</strong> que no hace\nnada — esa página es solo la tarjeta pública del bot y necesita un <strong>cliente de Telegram conectado</strong>\npara poder transferir. Usa la aplicación de escritorio/teléfono, o inicia sesión en\n<code>web.telegram.org</code> (vincula un dispositivo de escritorio escaneando el QR desde tu teléfono:\nConfiguración → Dispositivos → Vincular dispositivo de escritorio), luego envía un mensaje a BotFather allí.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"2-el-bot-devuelve-un-código-de-emparejamiento-en-lugar-de-responder\">2. El bot devuelve un código de emparejamiento en lugar de responder<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/#2-el-bot-devuelve-un-c%C3%B3digo-de-emparejamiento-en-lugar-de-responder\" class=\"hash-link\" aria-label=\"Enlace directo al 2. El bot devuelve un código de emparejamiento en lugar de responder\" title=\"Enlace directo al 2. El bot devuelve un código de emparejamiento en lugar de responder\" translate=\"no\">​</a></h3>\n<p>No es un error — es la <strong>puerta de emparejamiento</strong>. Los remitentes desconocidos deben ser aprobados primero\n(Paso 5). Después de la aprobación, el bot chatea normalmente.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"3-proxy-headers-detected-from-untrusted-address\">3. <code>Proxy headers detected from untrusted address</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/#3-proxy-headers-detected-from-untrusted-address\" class=\"hash-link\" aria-label=\"Enlace directo al 3-proxy-headers-detected-from-untrusted-address\" title=\"Enlace directo al 3-proxy-headers-detected-from-untrusted-address\" translate=\"no\">​</a></h3>\n<p>Si seguiste la Parte 2, la puerta de enlace registra esto porque Caddy está delante de ella.\nEs una advertencia, no un fallo. Para silenciarlo y restaurar la detección correcta del cliente,\nconfigura <code>gateway.trustedProxies</code> a la red de tu proxy.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"notas-de-seguridad-y-dimensionamiento\">Notas de seguridad y dimensionamiento<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/#notas-de-seguridad-y-dimensionamiento\" class=\"hash-link\" aria-label=\"Enlace directo al Notas de seguridad y dimensionamiento\" title=\"Enlace directo al Notas de seguridad y dimensionamiento\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong>El emparejamiento es la puerta</strong> — solo los usuarios de Telegram aprobados llegan al agente; el primer\nusuario aprobado se convierte en el propietario del comando.</li>\n<li class=\"\"><strong>Polling, no webhooks</strong> — no se abre ningún puerto de entrada, lo que mantiene la\nprotección de la Parte 2 intacta.</li>\n<li class=\"\"><strong>Dimensionamiento</strong> — en una caja de 2 vCPU, la puerta de enlace registró <code>event loop degraded</code> bajo carga.\nPara un bot ocupado, dale más 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=\"qué-sigue\">¿Qué sigue?<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al ¿Qué sigue?\" title=\"Enlace directo al ¿Qué sigue?\" translate=\"no\">​</a></h2>\n<p>Has terminado <strong>Parte 3</strong> de la serie <strong>Autoalojamiento de OpenClaw</strong>. A continuación:</p>\n<ul>\n<li class=\"\"><strong>Parte 4 — <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/\">Haz que OpenClaw sea privado con una VPN de malla NetBird</a></strong> <em>(siguiente)</em> — coloca tu agente en una malla privada y cierra los puertos públicos.</li>\n</ul>\n<p>Luego comienza la serie <strong>Autoalojamiento de Hermes</strong> — <strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/self-host-hermes-agent/\">Autoalojar el Agente Hermes con memoria persistente</a></strong> — para un agente cuyo contexto sobrevive a los reinicios.</p>\n<p>Los archivos complementarios para la serie se encuentran en el repositorio de manifiestos de WiLine <em>(enlace que vendrá con el repositorio)</em>.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"desmantelamiento\">Desmantelamiento<a href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/#desmantelamiento\" class=\"hash-link\" aria-label=\"Enlace directo al Desmantelamiento\" title=\"Enlace directo al Desmantelamiento\" translate=\"no\">​</a></h2>\n<p>Elimina el canal de Telegram (mantiene el resto de OpenClaw en funcionamiento):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Y <code>/revoke</code> el token del bot en BotFather si has terminado con él.</p>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/",
            "title": "Agregar un canal de Telegram a OpenClaw",
            "summary": "Habla con tu agente OpenClaw autoalojado desde tu teléfono. Crea un bot de Telegram, conéctalo, limpia la puerta de emparejamiento y chatea — capturado de una ejecución real.",
            "date_modified": "2026-06-23T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "autoalojamiento",
                "openclaw",
                "telegram",
                "chatbot"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/openclaw-wordmark-003352a1a7f02afc3bd877ae6f2dc175.png\" alt=\"OpenClaw\"><span class=\"tutorialHero__plus\">+</span><svg viewBox=\"0 0 24 24\" fill=\"#1F88C0\" aria-label=\"Caddy\" class=\"tutorialHero__docker\"><path d=\"M11.094.47c-.842 0-1.696.092-2.552.288a11.37 11.37 0 0 0-4.87 2.423 10.632 10.632 0 0 0-2.36 2.826A10.132 10.132 0 0 0 .305 8.582c-.398 1.62-.4 3.336-.043 5.048.085.405.183.809.31 1.212a11.85 11.85 0 0 0 1.662 3.729 3.273 3.273 0 0 0-.086.427 3.323 3.323 0 0 0 2.848 3.71 3.279 3.279 0 0 0 1.947-.346c1.045.51 2.17.864 3.339 1.04a11.66 11.66 0 0 0 4.285-.155 11.566 11.566 0 0 0 4.936-2.485 10.643 10.643 0 0 0 2.352-2.894 11.164 11.164 0 0 0 1.356-4.424 11.214 11.214 0 0 0-.498-4.335c.175-.077.338-.175.486-.293a.444.444 89.992 0 0 .001 0c.402-.322.693-.794.777-1.342a2.146 2.146 0 0 0-1.79-2.434 2.115 2.115 0 0 0-1.205.171c-.038-.043-.078-.086-.113-.13a11.693 11.693 0 0 0-3.476-2.93 13.348 13.348 0 0 0-1.76-.81 13.55 13.55 0 0 0-2.06-.613A12.121 12.121 0 0 0 11.093.47Zm.714.328c.345-.004.688.01 1.028.042a9.892 9.892 0 0 1 2.743.639c.984.39 1.89.958 2.707 1.632.803.662 1.502 1.45 2.091 2.328.026.039.048.08.07.12a2.12 2.12 0 0 0-.435 2.646c-.158.114-.97.692-1.634 1.183-.414.308-.733.557-.733.557l.581.68s.296-.276.665-.638c.572-.562 1.229-1.233 1.395-1.403a2.122 2.122 0 0 0 1.907.677 11.229 11.229 0 0 1-.013 4.046 11.41 11.41 0 0 1-1.475 3.897 12.343 12.343 0 0 1-2.079 2.587c-1.19 1.125-2.633 2.022-4.306 2.531a10.826 10.826 0 0 1-3.973.484 11.04 11.04 0 0 1-3.057-.652 3.304 3.304 0 0 0 1.417-2.294 3.275 3.275 0 0 0-.294-1.842c.18-.162.403-.363.656-.6 1.015-.955 2.353-2.303 2.353-2.303l-.47-.599s-1.63.972-2.801 1.728c-.307.198-.573.378-.777.517a3.273 3.273 0 0 0-1.516-.611c-1.507-.198-2.927.672-3.487 2.017a10.323 10.323 0 0 1-.695-1.078A10.92 10.92 0 0 1 .728 14.8a10.35 10.35 0 0 1-.2-1.212c-.164-1.653.103-3.258.629-4.754a12.95 12.95 0 0 1 1.087-2.288c.57-.968 1.248-1.872 2.069-2.656A11.013 11.013 0 0 1 11.808.797Zm-.147 3.257a3.838 3.838 0 0 0-3.82 3.82v2.36h-.94c-.751 0-1.377.625-1.377 1.377v3.8h1.46v-3.718h9.354v6.264H10.02v1.46h6.4c.751 0 1.377-.625 1.377-1.377v-6.43c0-.751-.626-1.377-1.377-1.377h-.94v-2.36a3.838 3.838 0 0 0-3.82-3.819zm0 1.46a2.371 2.371 0 0 1 2.36 2.36v2.36H9.3v-2.36a2.372 2.372 0 0 1 2.36-2.36zm10.141.392a1.253 1.253 0 0 1 1.296 1.434c-.049.319-.217.59-.453.78-.266.213-.61.318-.968.264a1.253 1.253 0 0 1-1.045-1.42 1.255 1.255 0 0 1 1.17-1.058zM5.384 17.425a2.02 2.02 0 0 1 1.917 1.298c.116.3.159.628.114.967a2.015 2.015 0 0 1-2.249 1.728 2.016 2.016 0 0 1-1.727-2.25 2.017 2.017 0 0 1 1.945-1.743z\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 6 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->6</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->6<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting OpenClaw</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Deploy your own AI assistant</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">2</span><span class=\"skillTracker__skill\" data-state=\"current\">Real HTTPS + auth</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Chat from Telegram</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Private mesh access</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/run-openclaw-on-wec-models/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run it on WEC models</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/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>En <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/\">Artículo 1</a> hicimos funcionar OpenClaw —\npero solo a través de HTTP sin cifrar, con un workaround <code>allowInsecureAuth</code>. Aquí colocamos\n<a href=\"https://caddyserver.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Caddy</a> frente a él como un proxy inverso: verdadero HTTPS,\nautenticación emparejada por dispositivo, y los puertos del gateway cerrados para que el proxy sea la única\nforma de acceso. Cada comando y error a continuación proviene de la implementación real.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>Continúa desde el Artículo 1 en la misma instancia WEC — <strong>Ubuntu 22.04.5 LTS</strong>, OpenClaw\n<strong>2026.6.8</strong>. Usamos <strong>Caddy 2</strong> (<code>caddy:2</code>). Debido a que esta máquina no tiene un dominio público,\nusamos la <strong>CA interna</strong> de Caddy (<code>tls internal</code>) para HTTPS autofirmado; la\nsección <a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#going-to-production\" class=\"\">Pasando a producción</a> muestra el cambio de una línea a certificados reales de\nLet's Encrypt una vez que tengas un dominio.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-construirás\">Lo que construirás<a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#lo-que-construir%C3%A1s\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que construirás\" title=\"Enlace directo al Lo que construirás\" translate=\"no\">​</a></h2>\n<p>Caddy termina HTTPS y hace un proxy inverso al gateway de OpenClaw a través de la\nred interna de Docker. El gateway deja de publicar sus propios puertos — el único punto de entrada\nse convierte en Caddy en <code>:443</code>.</p>\n<!-- -->\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/\">Artículo 1</a> completado</strong> — OpenClaw funcionando en <code>~/openclaw</code></li>\n<li class=\"\">Acceso a la terminal de la VM</li>\n<li class=\"\">Un nombre de host para el gateway. Con un dominio público, úsalo (y obtén certificados reales\nde Let's Encrypt). En una máquina privada, usamos <code>openclaw.local</code> + una entrada en hosts.</li>\n</ul>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--confirma-el-punto-de-partida\">Paso 1 — Confirma el punto de partida<a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#paso-1--confirma-el-punto-de-partida\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Confirma el punto de partida\" title=\"Enlace directo al Paso 1 — Confirma el punto de partida\" 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>Deberías ver <code>openclaw-gateway</code> <strong>Up (healthy)</strong>, publicando <code>18789/18790/3978</code>.\nEse es nuestro estado inicial: funcionando, pero solo HTTP.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker compose ps mostrando el gateway en funcionamiento\" src=\"data:image/png;base64,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\" 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=\"paso-2--escribe-el-caddyfile\">Paso 2 — Escribe el Caddyfile<a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#paso-2--escribe-el-caddyfile\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — Escribe el Caddyfile\" title=\"Enlace directo al Paso 2 — Escribe el Caddyfile\" translate=\"no\">​</a></h2>\n<p>La configuración de Caddy: servir HTTPS y hacer proxy a todo al gateway. <code>tls internal</code>\nusa la propia CA local de Caddy — no se necesita un dominio público.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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 selecciona su certificado por SNI (el nombre de host en el apretón de manos TLS).</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"># Una IP desnuda no envía SNI, así que usamos un nombre de host. Con un dominio público, coloca tu</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"># dominio aquí y elimina `tls internal` para Let's Encrypt automático.</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>El objetivo del proxy es <code>openclaw-gateway:18789</code> — Caddy accede al gateway por su\n<strong>nombre de servicio de Compose</strong> a través de la red compartida de Docker.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-3--agrega-caddy-a-compose\">Paso 3 — Agrega Caddy a Compose<a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#paso-3--agrega-caddy-a-compose\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — Agrega Caddy a Compose\" title=\"Enlace directo al Paso 3 — Agrega Caddy a Compose\" translate=\"no\">​</a></h2>\n<p><code>docker compose</code> fusiona automáticamente un <code>docker-compose.override.yml</code>, así que agregamos un servicio <code>caddy</code>\nsin tocar el archivo de compose oficial de OpenClaw:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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> persiste la CA local + certificados a través de reinicios.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-4--inicia-caddy\">Paso 4 — Inicia Caddy<a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#paso-4--inicia-caddy\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — Inicia Caddy\" title=\"Enlace directo al Paso 4 — Inicia 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 se inicia junto al gateway con <code>443</code> publicado, y genera su\nCA interna + un certificado en la primera ejecución.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Contenedor de Caddy en funcionamiento con 443 publicado\" src=\"https://development-wec.wiline.com/docs/es/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>nota</div><div class=\"admonitionContent_BuS1\"><p><code>docker compose up -d</code> también inicia el ayudante de un solo uso <code>openclaw-cli</code>; su inactividad\naquí es inofensiva — lo invocamos con <code>docker compose run --rm</code> cuando es necesario.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5--verifica-https\">Paso 5 — Verifica HTTPS<a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#paso-5--verifica-https\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 5 — Verifica HTTPS\" title=\"Enlace directo al Paso 5 — Verifica HTTPS\" translate=\"no\">​</a></h2>\n<p>Accede al endpoint de salud del gateway <strong>a través de Caddy</strong>. <code>--resolve</code> hace que curl envíe\nel nombre de host (SNI) mientras apunta a la máquina:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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 devolviendo a través de HTTPS vía 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>¿Recibes un <code>tlsv1 alert internal error</code>?</div><div class=\"admonitionContent_BuS1\"><p>Eso sucede si usaste una <strong>IP desnuda</strong> como dirección del sitio — consulta\n<a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#1-tlsv1-alert-internal-error\" class=\"\">Solución de problemas #1</a>. Usa un nombre de host.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-6--reasegura-el-gateway\">Paso 6 — Reasegura el gateway<a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#paso-6--reasegura-el-gateway\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 6 — Reasegura el gateway\" title=\"Enlace directo al Paso 6 — Reasegura el gateway\" translate=\"no\">​</a></h2>\n<p>Ahora el tráfico llega a través de <code>https://openclaw.local</code>. Dos cambios de configuración:</p>\n<ol>\n<li class=\"\">Agrega el <strong>origen</strong> HTTPS a <code>allowedOrigins</code> (la UI de Control rechaza orígenes desconocidos).</li>\n<li class=\"\">Desactiva <strong><code>allowInsecureAuth</code></strong> — HTTPS es un contexto seguro, por lo que el\nworkaround del Artículo 1 ya no es necesario.</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=\"paso-7--empareja-tu-navegador\">Paso 7 — Empareja tu navegador<a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#paso-7--empareja-tu-navegador\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 7 — Empareja tu navegador\" title=\"Enlace directo al Paso 7 — Empareja tu navegador\" translate=\"no\">​</a></h2>\n<p>Abre <strong><code>https://openclaw.local/</code></strong> en tu navegador (en una máquina privada, primero mapea\n<code>10.80.4.212 openclaw.local</code> en el archivo hosts de tu máquina — <code>/etc/hosts</code> en\nmacOS/Linux, o <code>C:\\Windows\\System32\\drivers\\etc\\hosts</code> editado como Administrador\nen Windows).</p>\n<p><strong>Primero recibirás una advertencia de certificado</strong> — <em>\"Tu conexión no es privada\"</em>\n(<code>NET::ERR_CERT_AUTHORITY_INVALID</code>). Esto es esperado con <code>tls internal</code>: el\ncertificado está firmado por la <strong>CA local</strong> de Caddy, que tu sistema operativo no\nconfía. Haz clic en <strong>Avanzado → Proceder a openclaw.local (inseguro)</strong> para continuar. (Con un\ndominio real + Let's Encrypt, el certificado es públicamente confiable y esta advertencia\nnunca aparece — consulta <a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#going-to-production\" class=\"\">Pasando a producción</a>.)</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Advertencia de certificado del navegador — haz clic en Avanzado, luego Proceder\" src=\"https://development-wec.wiline.com/docs/es/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>Ahora la página se carga — pero con <code>allowInsecureAuth</code> desactivado, el gateway requiere\n<strong>emparejamiento de dispositivo</strong> antes de que este navegador pueda usar la UI de Control. Ese es el flujo seguro haciendo su trabajo. La pantalla muestra un id de solicitud; apruébalo en el 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=\"Pantalla que requiere emparejamiento de dispositivo\" src=\"https://development-wec.wiline.com/docs/es/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>De vuelta en el navegador, haz clic en <strong>Conectar</strong> nuevamente — se empareja y carga el panel.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-8--cierra-el-acceso-directo-el-verdadero-endurecimiento\">Paso 8 — Cierra el acceso directo (el verdadero endurecimiento)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#paso-8--cierra-el-acceso-directo-el-verdadero-endurecimiento\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 8 — Cierra el acceso directo (el verdadero endurecimiento)\" title=\"Enlace directo al Paso 8 — Cierra el acceso directo (el verdadero endurecimiento)\" translate=\"no\">​</a></h2>\n<p>El gateway aún publica <code>18789</code> al host, por lo que <code>http://&lt;vm-ip&gt;:18789</code> es\n<em>todavía accesible en HTTP sin cifrar</em>, eludiendo a Caddy. Ciérralo: deja de publicar los\npuertos del gateway para que Caddy en <code>:443</code> sea la única puerta. Caddy aún puede acceder a él a través\nde la red interna.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">    # Deja de publicar los puertos del host del gateway — Caddy accede a él internamente, así que el</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    # único punto de entrada es HTTPS en :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> limpia los puertos que el compose base publicó.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-9--prueba-que-está-bloqueado\">Paso 9 — Prueba que está bloqueado<a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#paso-9--prueba-que-est%C3%A1-bloqueado\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 9 — Prueba que está bloqueado\" title=\"Enlace directo al Paso 9 — Prueba que está bloqueado\" 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;- HTTP directo (debería FALLAR)\"</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;- a través de Caddy (debería funcionar)\"</span><br></div></code></pre></div></div>\n<p>El primero no devuelve nada (conexión rechazada) — la puerta insegura ha desaparecido. El\nsegundo aún devuelve <code>{\"ok\":true,\"status\":\"live\"}</code>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Antes/después: puerto directo rechazado, HTTPS funciona\" src=\"data:image/png;base64,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\" 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>Tu OpenClaw ahora es accesible <strong>solo</strong> a través de HTTPS, emparejado por dispositivo.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"UI de Control de OpenClaw sobre HTTPS\" src=\"https://development-wec.wiline.com/docs/es/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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Ahora puedes <strong>poner verdadero HTTPS y autenticación frente a cualquier servicio autoalojado</strong> — y probar que\nla puerta trasera está cerrada.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"solución-de-problemas-errores-reales\">Solución de problemas (errores reales)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#soluci%C3%B3n-de-problemas-errores-reales\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas (errores reales)\" title=\"Enlace directo al Solución de problemas (errores reales)\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"1-tlsv1-alert-internal-error\">1. <code>tlsv1 alert internal error</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#1-tlsv1-alert-internal-error\" class=\"hash-link\" aria-label=\"Enlace directo al 1-tlsv1-alert-internal-error\" title=\"Enlace directo al 1-tlsv1-alert-internal-error\" translate=\"no\">​</a></h3>\n<p>Ocurre cuando la dirección del sitio en el Caddyfile era una IP desnuda (<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>Los registros de Caddy mostraron que el certificado se obtuvo correctamente. <strong>Causa:</strong> TLS selecciona un certificado\npor <strong>SNI</strong> (el nombre de host en el apretón de manos), pero las conexiones a una IP desnuda no envían SNI — así que Caddy no puede\ncoincidir un certificado y aborta. <strong>Solución:</strong> usa una dirección de sitio con nombre de host\n(Paso 2). Un lector con un dominio real nunca enfrenta esto — el dominio <em>es</em> el SNI.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"2-se-requiere-emparejamiento-de-dispositivo\">2. Se requiere emparejamiento de dispositivo<a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#2-se-requiere-emparejamiento-de-dispositivo\" class=\"hash-link\" aria-label=\"Enlace directo al 2. Se requiere emparejamiento de dispositivo\" title=\"Enlace directo al 2. Se requiere emparejamiento de dispositivo\" translate=\"no\">​</a></h3>\n<p>En la primera conexión HTTPS después de desactivar <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\">Se requiere emparejamiento de dispositivo — Este navegador necesita una aprobación única del host del Gateway</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">antes de que pueda usar la UI de Control.</span><br></div></code></pre></div></div>\n<p><strong>Causa:</strong> este es el flujo seguro <em>correcto</em> — con la autenticación insegura desactivada, cada nuevo\nnavegador debe ser aprobado. <strong>Solución:</strong> <code>docker compose run --rm openclaw-cli devices approve &lt;request-id&gt;</code>. (El CLI puede registrar <code>scope upgrade pending approval … using local fallback</code> pero aún completa la aprobación.)</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"3-neterr_cert_authority_invalid\">3. <code>NET::ERR_CERT_AUTHORITY_INVALID</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#3-neterr_cert_authority_invalid\" class=\"hash-link\" aria-label=\"Enlace directo al 3-neterr_cert_authority_invalid\" title=\"Enlace directo al 3-neterr_cert_authority_invalid\" translate=\"no\">​</a></h3>\n<p>Esperado con <code>tls internal</code> — el certificado está firmado por la CA local de Caddy, que tu\nsistema operativo no confía. Haz clic para continuar en una máquina privada. Con un dominio público + Let's\nEncrypt (a continuación), el certificado es confiable y no hay advertencia.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"pasando-a-producción\">Pasando a producción<a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#pasando-a-producci%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al Pasando a producción\" title=\"Enlace directo al Pasando a producción\" translate=\"no\">​</a></h2>\n<p>Con un dominio real, intercambia dos líneas en el Caddyfile — apúntalo a tu dominio y\nelimina <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>Luego apunta el <strong>registro A</strong> del dominio a la IP pública de la VM y abre <strong>80 + 443</strong>\na Internet. Caddy automáticamente provisiona y renueva un certificado de <strong>Let's Encrypt</strong>\n— confiable, sin advertencias, sin <code>--resolve</code> o <code>/etc/hosts</code> necesarios. Actualiza\n<code>allowedOrigins</code> a <code>https://openclaw.example.com</code>.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"notas-de-seguridad\">Notas de seguridad<a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#notas-de-seguridad\" class=\"hash-link\" aria-label=\"Enlace directo al Notas de seguridad\" title=\"Enlace directo al Notas de seguridad\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong>Único punto de entrada</strong> — solo el <code>:443</code> de Caddy está expuesto; el gateway no tiene puertos de host\n(Paso 8).</li>\n<li class=\"\"><strong>Emparejamiento de dispositivo</strong> — cada navegador necesita aprobación única (<code>openclaw devices</code>).</li>\n<li class=\"\"><strong>Lista de orígenes permitidos</strong> — la UI de Control solo acepta <code>https://openclaw.local</code>.</li>\n<li class=\"\"><strong>Rotar secretos</strong> — regenera <code>OPENCLAW_GATEWAY_TOKEN</code> si alguna vez ha sido\nmostrado (capturas de pantalla, comparticiones de pantalla).</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=\"qué-sigue\">¿Qué sigue?<a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al ¿Qué sigue?\" title=\"Enlace directo al ¿Qué sigue?\" translate=\"no\">​</a></h2>\n<p>Has terminado <strong>Parte 2</strong> de la serie <strong>Autoalojamiento de OpenClaw</strong>. A continuación:</p>\n<ul>\n<li class=\"\"><strong>Parte 3 — <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/\">Agregar un canal de Telegram</a></strong> <em>(siguiente)</em> — habla con tu agente ahora asegurado desde tu teléfono.</li>\n<li class=\"\"><strong>Parte 4 — <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/\">Hacer OpenClaw privado con una VPN de malla NetBird</a></strong> — colócalo en una malla privada y cierra los puertos públicos.</li>\n</ul>\n<p>Los archivos complementarios (<code>Caddyfile</code>, <code>docker-compose.override.yml</code>) se encuentran en el repositorio de manifiestos de WiLine <em>(enlace que vendrá con el repositorio)</em>.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"desmontaje\">Desmontaje<a href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/#desmontaje\" class=\"hash-link\" aria-label=\"Enlace directo al Desmontaje\" title=\"Enlace directo al Desmontaje\" translate=\"no\">​</a></h2>\n<p>Para eliminar solo Caddy y reabrir los puertos directos, elimina <code>docker-compose.override.yml</code>\ny <code>Caddyfile</code>, luego <code>docker compose up -d</code>. Para detener todo:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose down</span><br></div></code></pre></div></div>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/",
            "title": "Asegurar OpenClaw con un proxy inverso Caddy + HTTPS",
            "summary": "Coloca Caddy frente a OpenClaw para un verdadero HTTPS y autenticación emparejada por dispositivo, luego cierra los puertos del gateway para que el proxy sea la única forma de acceso — problemas y todo.",
            "date_modified": "2026-06-23T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "autoalojamiento",
                "docker",
                "openclaw",
                "caddy",
                "https",
                "seguridad"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/es/assets/images/openclaw-wordmark-003352a1a7f02afc3bd877ae6f2dc175.png\" alt=\"OpenClaw\"><span class=\"tutorialHero__plus\">+</span><svg viewBox=\"0 0 24 24\" fill=\"#2496ED\" aria-label=\"Docker\" class=\"tutorialHero__docker\"><path d=\"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z\"></path></svg><span class=\"tutorialHero__plus\">+</span><svg viewBox=\"0 0 24 24\" fill=\"#0a0a0a\" aria-label=\"OpenAI\" class=\"tutorialHero__openai\"><path d=\"M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997z\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 6 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->6</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->6<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting OpenClaw</div><ul class=\"skillTracker__steps\"><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">1</span><span class=\"skillTracker__skill\" data-state=\"current\">Deploy your own AI assistant</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real HTTPS + auth</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Chat from Telegram</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Private mesh access</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/tutorials/run-openclaw-on-wec-models/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run it on WEC models</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/es/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>Tu asistente de IA no tiene que vivir en la nube de otra persona.</p>\n<p>Despliega un agente de IA <a href=\"https://openclaw.ai/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">OpenClaw</a> autoalojado en una instancia WEC con\nDocker Compose — desde iniciar la VM hasta un agente que realmente responde, usando\ntu propia clave de API de modelo. Cada comando, versión y error a continuación fue capturado\nde un despliegue real en una instancia WEC.</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>Reproducibilidad</div><div class=\"admonitionContent_BuS1\"><p>Traes tu propia clave de API de modelo (OpenAI en esta guía); WiLine maneja el\nalojamiento. Esto se reprodujo en una instancia WEC — <strong>Ubuntu 22.04.5 LTS, 8 vCPU,\n15 GiB RAM, 25 GB de disco libre</strong>. Versión de OpenClaw <strong>2026.6.8</strong>, imagen\n<code>ghcr.io/openclaw/openclaw:latest</code>.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-construirás\">Lo que construirás<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#lo-que-construir%C3%A1s\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que construirás\" title=\"Enlace directo al Lo que construirás\" translate=\"no\">​</a></h2>\n<p>Un único contenedor <strong>gateway</strong> de OpenClaw, configurado con tu proveedor de modelo,\naccesible a través de su interfaz de Control UI web y CLI.</p>\n<!-- -->\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"requisitos-previos\">Requisitos previos<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#requisitos-previos\" class=\"hash-link\" aria-label=\"Enlace directo al Requisitos previos\" title=\"Enlace directo al Requisitos previos\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">Una cuenta de WiLine Edge Cloud</li>\n<li class=\"\">Una clave de API de modelo — esta guía utiliza <strong>OpenAI</strong>; Anthropic y modelos locales también funcionan</li>\n<li class=\"\">Un par de claves SSH en tu máquina (<code>~/.ssh/id_ed25519</code> o similar)</li>\n<li class=\"\">Aproximadamente 20 minutos</li>\n</ul>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-1--provisión-de-la-vm-en-wiline\">Paso 1 — Provisión de la VM en WiLine<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#paso-1--provisi%C3%B3n-de-la-vm-en-wiline\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 1 — Provisión de la VM en WiLine\" title=\"Enlace directo al Paso 1 — Provisión de la VM en WiLine\" translate=\"no\">​</a></h2>\n<p>Inicia una VM de WiLine que cumpla con estos requisitos:</p>\n<table><thead><tr><th>Requisito</th><th>Valor</th></tr></thead><tbody><tr><td>SO</td><td>Ubuntu 22.04 LTS (o 24.04 LTS)</td></tr><tr><td>Cómputo</td><td>≥ 2 vCPU / 4 GB RAM — OpenClaw necesita ≥ 2 GB; la primera ejecución puede ser OOM-killed (salida 137) en hosts de 1 GB</td></tr><tr><td>Disco</td><td>30 GB NVMe — 10 GB se llena rápido una vez que las imágenes de Docker y los registros se almacenan</td></tr><tr><td>Acceso</td><td>Tu <strong>clave pública SSH</strong> añadida en el momento del despliegue, para que puedas iniciar sesión sin contraseña</td></tr></tbody></table>\n<p>La guía del portal ya está documentada — sigue estos pasos y vuelve:</p>\n<ul>\n<li class=\"\"><strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/cloud_portal/platform/compute/instances/compute_instance/\">Desplegar una Máquina Virtual →</a></strong></li>\n<li class=\"\"><strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/es/cloud_portal/platform/compute/ssh_keys/\">Configurar una Clave SSH →</a></strong></li>\n</ul>\n<p>Una vez que esté en funcionamiento, inicia sesión por 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\">ssh</span><span class=\"token plain\"> ubuntu@</span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">tu-ip-vm</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=\"paso-2--verifica-la-máquina\">Paso 2 — Verifica la máquina<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#paso-2--verifica-la-m%C3%A1quina\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 2 — Verifica la máquina\" title=\"Enlace directo al Paso 2 — Verifica la máquina\" translate=\"no\">​</a></h2>\n<p>Antes de instalar cualquier cosa, confirma con qué estás trabajando — la versión del SO y\nlos recursos disponibles. Toma nota de estos números; son útiles cuando compares\nel rendimiento o abras un ticket de soporte.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_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>En nuestra instancia WEC: <code>Ubuntu 22.04.5 LTS</code>, <code>8</code> vCPU, ~<code>15 GiB</code> RAM, <code>25 GB</code> de disco libre.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal mostrando la versión del SO, CPU, RAM y disco\" src=\"https://development-wec.wiline.com/docs/es/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=\"paso-3--instalar-docker--compose\">Paso 3 — Instalar Docker + Compose<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#paso-3--instalar-docker--compose\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 3 — Instalar Docker + Compose\" title=\"Enlace directo al Paso 3 — Instalar Docker + Compose\" translate=\"no\">​</a></h2>\n<p>Verifica si Docker ya está presente:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Si no está instalado, sigue la guía oficial de Docker para tu distribución —\n<strong><a href=\"https://docs.docker.com/engine/install/ubuntu/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Instalar Docker Engine en Ubuntu</a></strong>.\nEl camino más rápido es el\n<a href=\"https://docs.docker.com/engine/install/#install-using-the-convenience-script\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">script de conveniencia de Docker</a>,\nseguido de añadir tu usuario al grupo <code>docker</code> para que puedas ejecutarlo sin\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>Necesitas <strong>Docker Compose v2</strong> (el subcomando <code>docker compose</code>, no el binario legado\n<code>docker-compose</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>nota</div><div class=\"admonitionContent_BuS1\"><p>Si Docker ya está presente, el script <code>get.docker.com</code> lo detecta y advierte\nen lugar de reinstalar — no lo fuerces en una máquina con contenedores en ejecución. En una\nVM limpia se instala desde cero. Este despliegue se realizó en un host donde Docker\n<strong>29.1.3 / Compose v5.0.2</strong> ya estaba instalado.</p></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker --version y salida de docker compose version\" 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=\"paso-4--obtener-el-archivo-compose\">Paso 4 — Obtener el archivo Compose<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#paso-4--obtener-el-archivo-compose\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 4 — Obtener el archivo Compose\" title=\"Enlace directo al Paso 4 — Obtener el archivo Compose\" translate=\"no\">​</a></h2>\n<p>Crea un directorio de proyecto y descarga el <code>docker-compose.yml</code> oficial de OpenClaw:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>El archivo define dos servicios — <code>openclaw-gateway</code> (el gateway de agente de larga duración)\ny <code>openclaw-cli</code> (una CLI de un solo uso que comparte la red del gateway).\nPor defecto, <strong>se construye desde el código fuente</strong>; en su lugar, lo apuntaremos a la imagen preconstruida.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-5--configurar-env\">Paso 5 — Configurar <code>.env</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#paso-5--configurar-env\" class=\"hash-link\" aria-label=\"Enlace directo al paso-5--configurar-env\" title=\"Enlace directo al paso-5--configurar-env\" translate=\"no\">​</a></h2>\n<p>Crea un <code>.env</code> junto al archivo compose. Fija la imagen preconstruida y establece un\ntoken de panel:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash 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\">pega la salida de: 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>Mantén <code>.env</code> fuera de git</div><div class=\"admonitionContent_BuS1\"><p>Contiene tu token de gateway (y cualquier clave). Agrégalo a <code>.gitignore</code>. En su lugar, compromete un\n<code>.env.example</code> con valores de marcador de posición.</p></div></div>\n<p>Descarga la imagen:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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 completando con la línea Pulled\" 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=\"paso-6--precrear-el-directorio-de-configuración\">Paso 6 — Precrear el directorio de configuración<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#paso-6--precrear-el-directorio-de-configuraci%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 6 — Precrear el directorio de configuración\" title=\"Enlace directo al Paso 6 — Precrear el directorio de configuración\" translate=\"no\">​</a></h2>\n<p>OpenClaw monta <code>~/.openclaw</code> en el contenedor. Créalo <strong>como tu propio\nusuario primero</strong> — de lo contrario, Docker lo crea como <code>root</code> y el contenedor (que\nse ejecuta como usuario <code>node</code>, uid 1000) no puede escribir en él:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Si omites esto, recibirás un error de permisos en la primera ejecución — consulta\n<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#troubleshooting-real-errors\" class=\"\">Solución de problemas</a>.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"paso-7--incorporación\">Paso 7 — Incorporación<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#paso-7--incorporaci%C3%B3n\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 7 — Incorporación\" title=\"Enlace directo al Paso 7 — Incorporación\" translate=\"no\">​</a></h2>\n<p>Aquí es donde traes tu propio modelo — la incorporación solicita tu proveedor y\nclave de API. Esta guía utiliza <strong>OpenAI</strong>, pero Anthropic y modelos locales funcionan de la misma manera.</p>\n<p>Ejecuta la incorporación de OpenClaw a través del contenedor gateway. Es interactivo — solicita\ntu proveedor y clave de API y escribe la configuración:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Sigue los pasos:</p>\n<ul>\n<li class=\"\"><strong>Continúa</strong> más allá del aviso de seguridad de uso personal → <strong>Sí</strong></li>\n<li class=\"\"><strong>Modo de configuración</strong> → <strong>QuickStart</strong></li>\n<li class=\"\"><strong>Proveedor</strong> → <strong>OpenAI</strong> (o tu proveedor), luego pega tu <strong>clave de API</strong></li>\n<li class=\"\"><strong>Modelo predeterminado</strong> → mantén el sugerido (<code>openai/gpt-5.5</code>)</li>\n<li class=\"\"><strong>Canal</strong> → busca <code>skip</code> → <strong>Saltar por ahora</strong> (los canales de mensajería son una guía posterior)</li>\n<li class=\"\"><strong>Habilidades / Hooks</strong> → <strong>No / Saltar por ahora</strong> (configura más tarde con\n<code>openclaw configure</code>)</li>\n<li class=\"\"><strong>Incubar tu agente</strong> → <strong>Incubar más tarde</strong> (iniciaremos el gateway como un servicio a continuación)</li>\n</ul>\n<p>Termina con <code>Incorporación completa.</code></p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Incorporación — seleccionando el proveedor OpenAI\" 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=\"Incorporación — &amp;quot;Modelo configurado: 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=\"paso-8--aplicar-la-configuración-del-gateway-y-comenzar\">Paso 8 — Aplicar la configuración del gateway y comenzar<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#paso-8--aplicar-la-configuraci%C3%B3n-del-gateway-y-comenzar\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 8 — Aplicar la configuración del gateway y comenzar\" title=\"Enlace directo al Paso 8 — Aplicar la configuración del gateway y comenzar\" translate=\"no\">​</a></h2>\n<p>La incorporación escribió tu proveedor y modelo. Ahora establece las opciones de tiempo de ejecución del gateway —\nmodo de ejecución, enlace de red y los orígenes de Control UI que aceptará — y luego inícialo\ncomo un servicio en segundo plano.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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 creando el contenedor gateway\" 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=\"paso-9--verificación-de-salud\">Paso 9 — Verificación de salud<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#paso-9--verificaci%C3%B3n-de-salud\" class=\"hash-link\" aria-label=\"Enlace directo al Paso 9 — Verificación de salud\" title=\"Enlace directo al Paso 9 — Verificación de salud\" translate=\"no\">​</a></h2>\n<p>Salud del contenedor:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Quieres <code>Up ... (healthy)</code>. Luego accede al endpoint de salud:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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 saludable y la respuesta JSON de healthz\" src=\"https://development-wec.wiline.com/docs/es/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>Confirma que tu clave de proveedor es utilizable:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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>Redactar antes de capturar pantalla</div><div class=\"admonitionContent_BuS1\"><p><code>models status</code> imprime un prefijo de clave enmascarado (<code>sk-proj-…</code>). Recorta o difumina antes de\npublicar la captura de pantalla.</p></div></div>\n<p>Ahora la verdadera prueba de extremo a extremo. El comando <code>agent</code> necesita un objetivo, así que lista los agentes\nprimero, luego envía un mensaje al predeterminado (<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 (predeterminado)</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\">\"Responde con exactamente una palabra: funcionando\"</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\"># funcionando</span><br></div></code></pre></div></div>\n<p>Si responde <code>funcionando</code>, toda la cadena — gateway → OpenAI → respuesta — está activa.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El agente respondiendo &amp;quot;funcionando&amp;quot; — la prueba\" src=\"https://development-wec.wiline.com/docs/es/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>La Control UI está en <code>http://&lt;tu-ip-instancia&gt;:18789/</code>; pega tu\n<code>OPENCLAW_GATEWAY_TOKEN</code> en Configuración para iniciar sesión.</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>Abrirlo desde otra máquina</div><div class=\"admonitionContent_BuS1\"><p>Los navegadores bloquean la Control UI a través de HTTP simple en una IP remota (\"Se requiere contexto de navegador seguro\"). Accede a ella a través de un túnel SSH para que se cargue como <code>127.0.0.1</code> — consulta\n<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#3-control-ui-secure-browser-context-required\" class=\"\">Solución de problemas #3</a>.</p></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Interfaz de Control de OpenClaw en el navegador, iniciada sesión\" src=\"https://development-wec.wiline.com/docs/es/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=\"solución-de-problemas-errores-reales\">Solución de problemas (errores reales)<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#soluci%C3%B3n-de-problemas-errores-reales\" class=\"hash-link\" aria-label=\"Enlace directo al Solución de problemas (errores reales)\" title=\"Enlace directo al Solución de problemas (errores reales)\" translate=\"no\">​</a></h2>\n<p>Estos son errores que realmente encontramos durante este despliegue — no hipotéticos.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"1-eacces-permiso-denegado-mkdir-homenodeopenclawstate\">1. <code>EACCES: permiso denegado, mkdir '/home/node/.openclaw/state'</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#1-eacces-permiso-denegado-mkdir-homenodeopenclawstate\" class=\"hash-link\" aria-label=\"Enlace directo al 1-eacces-permiso-denegado-mkdir-homenodeopenclawstate\" title=\"Enlace directo al 1-eacces-permiso-denegado-mkdir-homenodeopenclawstate\" translate=\"no\">​</a></h3>\n<p>Ocurrió en la primera ejecución de incorporación cuando <code>~/.openclaw</code> no existía previamente:</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] El comando CLI falló.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">[openclaw] Razón: Falló al abrir la base de datos de estado del plugin.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">| EACCES: permiso denegado, mkdir '/home/node/.openclaw/state' | EACCES</span><br></div></code></pre></div></div>\n<p><strong>Causa:</strong> Docker creó automáticamente la fuente de montaje vinculada <code>~/.openclaw</code> propiedad de\n<code>root</code> (<code>0:0</code>). El contenedor se ejecuta como usuario <code>node</code> (uid 1000), por lo que no puede escribir\ndentro de ese directorio de propiedad de root.</p>\n<p><strong>Solución:</strong> da el directorio al uid 1000 (que también es tu usuario <code>ubuntu</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\">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>Precrear el directorio tú mismo (Paso 6) evita esto por completo.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Error de permisos EACCES en la terminal\" src=\"https://development-wec.wiline.com/docs/es/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-se-seleccionó-sesión-de-destino\">2. <code>No se seleccionó sesión de destino</code><a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#2-no-se-seleccion%C3%B3-sesi%C3%B3n-de-destino\" class=\"hash-link\" aria-label=\"Enlace directo al 2-no-se-seleccionó-sesión-de-destino\" title=\"Enlace directo al 2-no-se-seleccionó-sesión-de-destino\" translate=\"no\">​</a></h3>\n<p>La primera vez que ejecutamos <code>agent</code> sin especificar a quién hablar:</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 se seleccionó sesión de destino. Usa --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;, o --to &lt;E.164&gt;. Ejecuta openclaw agents list para ver agentes.</span><br></div></code></pre></div></div>\n<p><strong>Causa:</strong> <code>openclaw agent</code> ejecuta un turno contra un agente/sesión específica; sin\nun canal configurado no hay un objetivo implícito.</p>\n<p><strong>Solución:</strong> lista los agentes y pasa el id explícitamente:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\"># muestra: main (predeterminado)</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=\"El error &amp;quot;No se seleccionó sesión de destino&amp;quot;\" src=\"https://development-wec.wiline.com/docs/es/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-se-requiere-contexto-de-navegador-seguro\">3. Control UI: \"Se requiere contexto de navegador seguro\"<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#3-control-ui-se-requiere-contexto-de-navegador-seguro\" class=\"hash-link\" aria-label=\"Enlace directo al 3. Control UI: &quot;Se requiere contexto de navegador seguro&quot;\" title=\"Enlace directo al 3. Control UI: &quot;Se requiere contexto de navegador seguro&quot;\" translate=\"no\">​</a></h3>\n<p>Abrir la Control UI desde otra máquina (por ejemplo, macOS) en la IP de la VM a través de\nHTTP simple, la página se niega a conectar:</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\">Se requiere contexto de navegador seguro</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Esta página se está ejecutando a través de HTTP simple, por lo que el navegador no puede crear la</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">identidad del dispositivo que espera el Gateway.</span><br></div></code></pre></div></div>\n<p><strong>Causa:</strong> los navegadores solo exponen las API criptográficas que OpenClaw necesita en un <em>contexto seguro</em> — HTTPS, o <code>localhost</code>/<code>127.0.0.1</code>. Un <code>http://&lt;ip&gt;:18789</code> remoto no es ninguno de los dos, por lo que está bloqueado en el <strong>navegador</strong>, independientemente de la bandera del lado del servidor\n<code>gateway.controlUi.allowInsecureAuth</code>.</p>\n<p><strong>Solución:</strong> tunela el puerto a través de SSH para que el navegador hable con <code>127.0.0.1</code> (un contexto seguro). En tu máquina local:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" 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\">tu-ip-vm</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><br></div></code></pre></div></div>\n<p>Luego abre <code>http://127.0.0.1:18789/#token=&lt;tu-token&gt;</code> localmente. La solución adecuada a largo plazo es HTTPS a través de un proxy inverso — la próxima guía.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"El mensaje rojo &amp;quot;Se requiere contexto de navegador seguro&amp;quot; en la Control UI\" src=\"https://development-wec.wiline.com/docs/es/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=\"notas-de-seguridad\">Notas de seguridad<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#notas-de-seguridad\" class=\"hash-link\" aria-label=\"Enlace directo al Notas de seguridad\" title=\"Enlace directo al Notas de seguridad\" translate=\"no\">​</a></h2>\n<p>Al iniciar, el gateway registró advertencias que vale la pena actuar antes de cualquier exposición pública:</p>\n<ul>\n<li class=\"\"><strong>Vinculando a una dirección no de bucle invertido</strong> — el gateway escucha en la LAN. No\nexpongas el puerto 18789 a Internet público sin autenticación delante de él.</li>\n<li class=\"\"><strong><code>gateway.controlUi.allowInsecureAuth=true</code></strong> — marcado como peligroso. Ejecuta\n<code>docker compose run --rm openclaw-cli security audit</code>.</li>\n<li class=\"\"><strong><code>plugins.allow</code> está vacío</strong> — los plugins no empaquetados (por ejemplo, <code>codex</code>) pueden\ncargarse automáticamente. Establece una lista de permitidos explícita de ids de plugins de confianza.</li>\n</ul>\n<p>Endurecer esto detrás de un proxy inverso con HTTPS es el tema de la próxima guía.</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>Habilidad desbloqueada 🏅</div><div class=\"admonitionContent_BuS1\"><p>Ahora puedes <strong>desplegar y operar tu propio agente de IA</strong> en una máquina que controlas.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"lo-que-has-logrado\">Lo que has logrado<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#lo-que-has-logrado\" class=\"hash-link\" aria-label=\"Enlace directo al Lo que has logrado\" title=\"Enlace directo al Lo que has logrado\" translate=\"no\">​</a></h2>\n<p>Pasaste de una instancia WEC vacía a un agente de IA autoalojado en funcionamiento:</p>\n<ul>\n<li class=\"\">Provisionaste y verificaste una VM de Ubuntu en WEC</li>\n<li class=\"\">Levantaste el gateway de OpenClaw con Docker Compose y la imagen preconstruida oficial</li>\n<li class=\"\">Conectaste tu propio proveedor de modelo (OpenAI) y confirmaste que funciona de extremo a extremo — el agente\nrespondió a través de la CLI y la Control UI</li>\n<li class=\"\">Encontraste (y solucionaste) los problemas reales de permisos, sesión y contexto seguro en el camino</li>\n</ul>\n<p>El agente es tuyo: tu clave, tu máquina, tus datos. A partir de aquí se trata de hacerlo\nseguro, accesible y útil.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"qué-puedes-hacer-con-él\">¿Qué puedes hacer con él?<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#qu%C3%A9-puedes-hacer-con-%C3%A9l\" class=\"hash-link\" aria-label=\"Enlace directo al ¿Qué puedes hacer con él?\" title=\"Enlace directo al ¿Qué puedes hacer con él?\" translate=\"no\">​</a></h2>\n<p>OpenClaw no es un chatbot — es un <strong>agente autónomo que puede actuar en tu máquina</strong>. Ahora\nque está en funcionamiento, aquí hay lo que desbloquea (algunas funciones necesitan configuración adicional, y\ncubrimos las más importantes más adelante en esta serie):</p>\n<ul>\n<li class=\"\"><strong>Ejecutar trabajo real en la máquina.</strong> Puede ejecutar comandos de shell y leer/escribir archivos\n— <em>\"ver qué está usando el disco,\"</em> <em>\"seguir los registros y resumir los errores\"</em> — con\nsandboxing configurable sobre cuánto le permites tocar.</li>\n<li class=\"\"><strong>Hablar con él desde tu teléfono.</strong> OpenClaw se conecta a <strong>WhatsApp, Telegram, Discord,\nSlack, Signal e iMessage</strong>, por lo que delegas tareas conversacionalmente sin SSH.\n<em>(Conectamos Telegram en la próxima guía.)</em></li>\n<li class=\"\"><strong>Navegar y actuar en la web.</strong> La automatización de navegador incorporada le permite navegar por sitios,\nllenar formularios y extraer datos de forma autónoma.</li>\n<li class=\"\"><strong>Escribir y ejecutar código.</strong> Se integra con <strong>Claude Code</strong> para codificación autónoma —\nescribiendo y modificando código, ejecutando pruebas e incluso abriendo PRs.</li>\n<li class=\"\"><strong>Trabajar en segundo plano, proactivamente.</strong> Se ejecuta 24/7, puede ejecutar tareas en segundo plano,\ny realiza chequeos de \"latido\" en lugar de solo reaccionar cuando le envías un mensaje.</li>\n<li class=\"\"><strong>Crecer contigo.</strong> Una biblioteca de habilidades comunitarias (<strong>ClawHub</strong>), herramientas autoescritas y\norquestación de múltiples agentes significan que se vuelve más capaz con el tiempo — respaldado por una memoria persistente que lleva contexto a través de conversaciones.</li>\n</ul>\n<p>Usos reales típicos que la gente realiza: gestión de bandejas de entrada y calendarios, procesamiento de documentos,\ntareas de infraestructura en la máquina, tuberías de contenido y pruebas de código autónomas.</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=\"qué-sigue\">¿Qué sigue?<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#qu%C3%A9-sigue\" class=\"hash-link\" aria-label=\"Enlace directo al ¿Qué sigue?\" title=\"Enlace directo al ¿Qué sigue?\" translate=\"no\">​</a></h2>\n<p>Esta es <strong>Parte 1</strong> de la serie <strong>Autoalojamiento de OpenClaw</strong> sobre cómo ejecutar infraestructura de IA de calidad de producción en una instancia WEC. Próximamente:</p>\n<ul>\n<li class=\"\"><strong>Parte 2 — <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/secure-openclaw-caddy-https/\">Asegurar OpenClaw con un proxy inverso Caddy + HTTPS</a></strong> <em>(siguiente)</em> — pon HTTPS delante del gateway para que puedas eliminar la solución alternativa <code>allowInsecureAuth</code> y acceder a la Control UI desde cualquier lugar, de forma segura.</li>\n<li class=\"\"><strong>Parte 3 — <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/add-telegram-channel-openclaw/\">Agregar un canal de Telegram</a></strong> — habla con tu agente desde tu teléfono.</li>\n<li class=\"\"><strong>Parte 4 — <a class=\"\" href=\"https://development-wec.wiline.com/docs/es/tutorials/netbird-mesh-vpn-openclaw/\">Hacer OpenClaw privado con una VPN de malla NetBird</a></strong> — colócalo en una malla privada y cierra los puertos públicos.</li>\n</ul>\n<p>Los archivos complementarios para esta guía — <code>docker-compose.yml</code> y <code>.env.example</code> — se encuentran en\nel repositorio de manifiestos de WiLine <em>(enlace que vendrá con el repositorio)</em>.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"desmontaje\">Desmontaje<a href=\"https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/#desmontaje\" class=\"hash-link\" aria-label=\"Enlace directo al Desmontaje\" title=\"Enlace directo al Desmontaje\" 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\"># detener + eliminar contenedores</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\"># también eliminar la imagen y volúmenes</span><br></div></code></pre></div></div>\n<p>Para eliminar la configuración/estado en el host:\n<code>rm -rf ~/.openclaw ~/.openclaw-auth-profile-secrets</code>. Para detener la facturación por completo,\nelimina la VM desde el portal de WiLine.</p>",
            "url": "https://development-wec.wiline.com/docs/es/tutorials/deploy-openclaw-docker-compose/",
            "title": "Desplegar OpenClaw en una instancia WEC a través de Docker Compose",
            "summary": "Desde una instancia WEC nueva hasta un agente OpenClaw autoalojado que realmente responde — Docker Compose, tu propia clave de modelo y cada error real y solución de un despliegue en vivo.",
            "date_modified": "2026-06-18T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "autoalojamiento",
                "docker",
                "openclaw",
                "vps"
            ]
        }
    ]
}