Enmascarar PII en la puerta de enlace: configurar Presidio, además de la línea que los documentos omiten
+La Parte 2 terminó con la puerta de enlace decidiendo qué modelo responde a cada solicitud — y aún así reenvía cada solicitud tal cual, incluyendo las que llevan nombres de clientes, correos electrónicos y números de teléfono.
Esta publicación coloca un filtro de PII en ese camino. No delante del modelo, sin embargo: delante de los registros. El modelo que lee tu solicitud está haciendo su trabajo; el riesgo es lo que se almacena. Así que el texto sin procesar va al modelo y una copia enmascarada va a tus registros.
Eso es lo que la puerta de enlace publicita. Seguir su configuración documentada me dio
lo opuesto: un modelo que respondió correctamente y una respuesta que regresó como
<LOCATION>. Así es como encontrar eso y cómo solucionarlo.
LiteLLM 1.96.2 (ghcr.io/berriai/litellm:main-stable), imágenes de Presidio de
ghcr.io/data-privacy-stack, modelos qwen-small (Qwen2.5-3B-Instruct) y
qwen-mid (Qwen3.5-9B) a través de la API de Inferencia WEC, el 20 de agosto de 2026. Los valores predeterminados
cambian — si tus resultados difieren, verifica primero tu versión.
Qué es Presidio y dónde se coloca
Presidio detecta datos personales en texto: nombres, correos electrónicos, números de teléfono, números de tarjeta. Le envías una cadena y devuelve las entidades que encontró, con desplazamientos de caracteres y un puntaje de confianza.
Se envía como dos servicios: un analizador que encuentra PII y un anonimizador que lo reemplaza. La puerta de enlace llama a ambos.
Una nota sobre nombres que confunde a la gente: Presidio era el proyecto de Microsoft y la mayoría
de los tutoriales aún lo llaman así. El repositorio se trasladó a la organización data-privacy-stack
y github.com/microsoft/presidio ahora responde Moved Permanently. Mismo proyecto, nuevo hogar, que es por qué las imágenes a continuación no están bajo
una ruta de Microsoft.
Agregar Presidio al compose de la puerta de enlace
Presidio solo habla con la puerta de enlace, por lo que no necesita puertos publicados.
Abre el docker-compose.yml de la Parte 1 y agrega dos servicios después del bloque db:
, encima de volumes::
presidio-analyzer:
image: ghcr.io/data-privacy-stack/presidio-analyzer:latest
container_name: presidio-analyzer
restart: unless-stopped
presidio-anonymizer:
image: ghcr.io/data-privacy-stack/presidio-anonymizer:latest
container_name: presidio-anonymizer
restart: unless-stopped
Dos espacios de sangría, al mismo nivel que litellm: y db:.
Nota lo que está ausente: no hay ports:. Colocarlos en el mismo proyecto de compose que
la puerta de enlace significa que Docker les da una red y DNS compartidos, por lo que la puerta de enlace
los alcanza por nombre de servicio y nada más en tu red puede alcanzarlos. Si desplegaste Presidio por separado con ports: ["5002:3000"], eso
expondría tu analizador de PII a todo lo que pueda enrutar hacia el host — vale la pena deshacerlo.
cd ~/llm-gateway && docker compose up -d
docker compose ps
Cuatro contenedores, y las filas de Presidio deberían mostrar 3000/tcp sin mapeo de host:
NAME SERVICE STATUS PORTS
llm-gateway litellm Up 127.0.0.1:4000->4000/tcp
llm-gateway-db db Up (healthy) 5432/tcp
presidio-analyzer presidio-analyzer Up (healthy) 3000/tcp
presidio-anonymizer presidio-anonymizer Up (healthy) 3000/tcp

Si docker compose ps muestra solo dos contenedores, el archivo de compose no se guardó.
Confirma lo que compose está leyendo realmente:
docker compose config --services
Eso debe listar cuatro nombres. Lee el archivo en disco, así que captura una edición que nunca salió de tu editor.
Verifica el analizador y aprende sus límites
Antes de conectar nada, ve lo que Presidio realmente encuentra:
No hay puertos de host ahora, así que pregunta desde dentro de la puerta de enlace. La imagen de LiteLLM
no incluye curl, así que usa su Python:
docker exec llm-gateway python -c "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 [email protected]','language':'en'}).encode(), headers={'Content-Type':'application/json'}); print(u.urlopen(r).read().decode())"
[{"entity_type": "EMAIL_ADDRESS", "score": 1.0, "start": 27, "end": 45},
{"entity_type": "PERSON", "score": 0.85, "start": 5, "end": 11},
{"entity_type": "URL", "score": 0.5, "start": 34, "end": 45}]

Lee eso cuidadosamente, porque establece expectativas para todo lo que sigue.
Encontró el correo electrónico con plena confianza y el nombre en 0.85. No encontró el
número de teléfono en absoluto. 555-0142 son siete dígitos sin código de área, y
Presidio valida contra planes de numeración reales en lugar de hacer coincidir patrones
que se vean como teléfonos. Agrega un código de área:
docker exec llm-gateway python -c "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 [email protected]','language':'en'}).encode(), headers={'Content-Type':'application/json'}); print(u.urlopen(r).read().decode())"
Ahora aparece PHONE_NUMBER — puntuando 0.4, frente a 1.0 para el correo electrónico.
Vale la pena verificar si el prefijo 555 es el problema, ya que ese rango está
reservado para ficción. No lo es. Un número plausible se comporta de manera idéntica:
docker exec llm-gateway python -c "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())"
[{"entity_type": "PERSON", "score": 0.85, "start": 5, "end": 11},
{"entity_type": "PHONE_NUMBER", "score": 0.4, "start": 15, "end": 29}]
Misma puntuación de 0.4 para el número completo, y el bare 682-4531 sigue sin ser detectado. Así que dos
cosas son ciertas: un código de área es lo que hace que un número de teléfono sea detectable,
y 0.4 es simplemente lo que este reconocedor devuelve para números de teléfono — no una
penalización por números falsos.
Lo que significa que los tipos de entidad no son igualmente confiables, y la confianza es un número
que querrás establecer deliberadamente. Presidio expone presidio_score_thresholds
para exactamente eso: un umbral por entidad por debajo del cual se descartan las detecciones. Establecerlo
en 0.5 y dejarías de enmascarar silenciosamente cada número de teléfono en tu tráfico.
No construyas una historia de cumplimiento sobre la suposición de que esto captura todo.
Enseñándole los números que se pierde
Hay una salida, y vale la pena saberlo antes de decidir que la cobertura
es inaceptable. presidio_ad_hoc_recognizers toma una ruta a un archivo JSON de
reconocedores adicionales, cargados cuando se inicia la guardia:
[
{
"name": "LocalPhoneRecognizer",
"supported_language": "en",
"supported_entity": "PHONE_NUMBER",
"patterns": [
{ "name": "us-local-7", "regex": "\\b\\d{3}-\\d{4}\\b", "score": 0.9 }
]
}
]
Móntalo junto a la configuración y apunta la guardia hacia él:
- ./recognizers.json:/app/recognizers.json:ro
presidio_ad_hoc_recognizers: /app/recognizers.json
Recrea el contenedor — un nuevo volumen necesita up -d, no restart — y la misma
solicitud que se filtró ahora se enmascara:
Respuesta OK. Contacta a <PERSON> al <PHONE_NUMBER> sobre la factura.
Por qué la puntuación es 0.9 y no 0.8. Coloca 682-4531 dentro de una oración —
Llama a 682-4531 — y los reconocedores predeterminados devuelven DATE_TIME en
0.85: siete dígitos con un guion, leídos como una fecha. (Pasa el número por sí solo,
sin oración alrededor, y nada se activa; el modelo NER quiere
contexto antes de comprometerse a nada.) A 0.8 tu reconocedor de teléfono pierde la
superposición y el número se enmascara como <DATE_TIME>: sigue redactado, pero archivado
bajo la entidad equivocada, lo que rompe silenciosamente cualquier informe que cuente por tipo.
A 0.86 o más PHONE_NUMBER gana.
Medido en tres ejecuciones cada uno, completamente determinista:
| Texto | Predeterminado | Reconocedor a 0.8 | Reconocedor a 0.9 |
|---|---|---|---|
Llama a 555-0142 | dejado en claro | <PHONE_NUMBER> | <PHONE_NUMBER> |
Llama a 682-4531 | <DATE_TIME> | <DATE_TIME> | <PHONE_NUMBER> |
(415) 682-4531 o al 682-4531 | uno bare filtra | ambos enmascarados | ambos enmascarados |
Nota que la fila del medio no es un fallo — es una clasificación errónea, y el anonimizador aún lo redacta. Las primeras y terceras filas son las verdaderas filtraciones, y un reconocedor cierra ambas.
La versión anterior no es segura para enviar
Siete dígitos y un guion son una forma, no un significado, y muchas cosas comparten eso. Ejecuta ese reconocedor contra texto de una cola de soporte real:
| Texto | Enmascarado como |
|---|---|
El pedido 482-1099 se envió ayer | <PHONE_NUMBER> |
La factura 100-2000 está vencida | <PHONE_NUMBER> |
El número de parte 250-4000 está descontinuado | <PHONE_NUMBER> |
El código de error era 500-1001 | <PHONE_NUMBER> |
Nuestra oficina está en 200-4500 Main Street | <PHONE_NUMBER> |
RFC 793-1981 define TCP | <PHONE_NUMBER> |
Seis de seis. Y el 0.9 que configuraste para ganar la superposición de DATE_TIME ahora gana cada
superposición, por lo que el reconocedor no solo agrega ruido — sobrescribe clasificaciones correctas con una incorrecta. Rastreos llenos de <PHONE_NUMBER> donde solían estar los números de pedido son peores que rastreos con un número en claro, porque
ahora no puedes decir cuál es cuál.
La solución es dejar de coincidir solo por forma y requerir una palabra clave delante de los
dígitos. Eso tiene que ser un lookbehind: Presidio redacta toda la coincidencia, así que
(call\W+)(\d{3}-\d{4}) tragaría la palabra "llama" junto con el número —
los grupos de captura no reducen el alcance. El re de Python solo permite lookbehinds de ancho fijo,
que es por qué esto es una alternancia en lugar de una lista ordenada:
[
{
"name": "LocalPhoneRecognizer",
"supported_language": "en",
"supported_entity": "PHONE_NUMBER",
"patterns": [
{
"name": "us-local-7-cued",
"regex": "(?i)(?:(?<=call )|(?<=called )|(?<=calling )|(?<=phone )|(?<=phone: )|(?<=phone is )|(?<=tel )|(?<=tel: )|(?<=cell )|(?<=cell: )|(?<=cell is )|(?<=mobile )|(?<=mobile: )|(?<=contact )|(?<=contact: )|(?<=number )|(?<=number: )|(?<=number is )|(?<=me on )|(?<=him on )|(?<=her on )|(?<=them on )|(?<=us on )|(?<=me at )|(?<=him at )|(?<=her at )|(?<=them at )|(?<=us at ))(?<!part number )(?<!serial number )(?<!order number )(?<!invoice number )(?<!account number )(?<!model number )(?<!tracking number )(?<!reference number )(?<!batch number )(?<!ticket number )(?<!case number )\\d{3}-\\d{4}\\b",
"score": 0.9
}
]
}
]
Los lookbehinds negativos finales están ahí porque number es una pista útil y
part number no lo es.
Medido en doce frases de teléfono y doce similares:
\b\d{3}-\d{4}\b | Anclado por pista | |
|---|---|---|
| Números de teléfono enmascarados | 12/12 | 12/12 |
| Similares enmascarados erróneamente | 12/12 | 0/12 |
Dos cosas que esto no soluciona, y debes conocer ambas. El pedido 482-1099 sigue
regresando como <DATE_TIME> — eso es un fallo de spaCy por sí mismo, con o sin
tu reconocedor. Y un número de teléfono introducido por una frase que no pensaste
vuelve a filtrarse. Una lista de pistas es una suposición sobre cómo la gente escribe, así que trátala
como algo que revisar contra tu propio tráfico en lugar de un artefacto terminado.
BR está en la lista de regiones predeterminadas, así que (11) 91234-5678 es reconocido — y
91234-5678 sin el DDD no lo es, exactamente como con un código de área de EE. UU. faltante. El
mismo reconocedor lo cierra con \d{4,5}-\d{4} y una lista de pistas en portugués.
Conectar la guardia
La puerta de enlace llama a Presidio a través de una guardia. Abre config.yaml y agrega un
bloque guardrails: al final — esta es la configuración que la documentación de LiteLLM
da para enmascarar solo en el camino de registro:
guardrails:
- guardrail_name: presidio-log-mask
litellm_params:
guardrail: presidio
mode: logging_only
default_on: true
presidio_analyzer_api_base: http://presidio-analyzer:3000
presidio_anonymizer_api_base: http://presidio-anonymizer:3000
mode: logging_only es el importante. La documentación lo describe como:
Ejecutar después de la llamada LLM, aplicar solo el enmascaramiento de PII antes de registrar en Langfuse, etc. No en la solicitud / respuesta real de la API llm.
default_on: true lo aplica a cada solicitud en lugar de solo a aquellas que lo piden
por nombre. Y los dos valores api_base son los nombres de servicio del archivo
compose — la guardia se ejecuta dentro del contenedor de la puerta de enlace, así que localhost
apuntaría al lugar equivocado.
Reinicia y espera a que el proxy realmente regrese — toma de 30 a 60
segundos, que es más largo que la mayoría de los comandos sleep que la gente pone delante de él:
docker compose restart litellm
until curl -sf localhost:4000/health/readiness >/dev/null 2>&1; do printf '.'; sleep 3; done; echo " listo"
Luego confirma que la guardia se cargó, lo cual es más confiable que leer registros:
KEY=$(grep -m1 LITELLM_MASTER_KEY ~/llm-gateway/.env | cut -d= -f2- | tr -d '"') && \
curl -s localhost:4000/guardrails/list -H "Authorization: Bearer $KEY" \
| python3 -c "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))"
El punto final devuelve cada posible campo de guardia, la mayoría de ellos null, así que esto
selecciona los que importan:
{
"guardrail_name": "presidio-log-mask",
"guardrail": "presidio",
"mode": "logging_only",
"presidio_filter_scope": "input",
"default_on": true,
"presidio_analyzer_api_base": "http://presidio-analyzer:3000",
"presidio_anonymizer_api_base": "http://presidio-anonymizer:3000",
"fail_on_error": true,
"unreachable_fallback": "fail_closed"
}
Esos últimos dos son los valores predeterminados que vale la pena conocer antes de que esto vea tráfico real: si Presidio no está disponible, las solicitudes fallan en lugar de pasar sin enmascarar.

Pruébalo y obtén una sorpresa
Ahora la parte que importa. Envía una solicitud cuya respuesta pruebe si el modelo vio el texto real — pedirle que repita la PII de vuelta es inútil, porque la respuesta también se escanea:
KEY=$(grep -m1 LITELLM_MASTER_KEY ~/llm-gateway/.env | cut -d= -f2- | tr -d '"') && \
curl -s localhost:4000/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"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."}]}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['choices'][0]['message']['content'])"
Esperado: San Francisco, probando que el modelo leyó los dígitos.
Real:
<LOCATION>

El modelo respondió a una pregunta que requiere leer un código de área real — y luego
la respuesta misma regresó enmascarada. logging_only se suponía que debía permanecer fuera de
la respuesta real.
Por qué sucede eso
Hay una segunda configuración, y su valor predeterminado está haciendo el trabajo.
presidio_filter_scope controla qué dirección se escanea. De la
documentación:
input: solo se escanea el contenido de usuario → modelo;output: solo se escanea el contenido de modelo → usuario;both(predeterminado): escanear ambas direcciones
Y, en la misma página:
Usa
presidio_filter_scope: output(oboth) cuando quieras que Presidio escanee y enmascare activamente la respuesta del modelo antes de que llegue al usuario.
Así que both — el predeterminado — enmascara activamente las respuestas. El ejemplo documentado de logging_only
no establece presidio_filter_scope en absoluto, lo que lo deja en both.
El resultado es una configuración que enmascara la cosa que la misma página dice que no
tocará.
En el código fuente, initialize_presidio lee ese alcance y registra un segundo
callback con apply_to_output=True en post_call siempre que el escaneo de salida esté
habilitado. El gancho de evento de ese callback está codificado, por lo que el mode que estableces nunca
llega a él.
Esto ha sido reportado.
Issue #30447, "la guardia de Presidio en modo logging_only corrompe la respuesta orientada al usuario (contenido del asistente reemplazado con tokens de PII)", 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 "fix(presidio): no enmascarar la solicitud en vivo cuando la guardia es logging_only". El informe trataba sobre la respuesta; la solución abordó la solicitud. En la imagen main-stable utilizada aquí, el comportamiento anterior aún se reproduce.
Un segundo informe está abierto:
#35951 señala que en
modo logging_only el gancho de enmascaramiento reescribe kwargs["messages"] pero no
propaga a standard_logging_object, que es lo que leen los registradores posteriores.
La solución
Una línea. Agrega presidio_filter_scope: input a la guardia:
guardrails:
- guardrail_name: presidio-log-mask
litellm_params:
guardrail: presidio
mode: logging_only
presidio_filter_scope: input
default_on: true
presidio_analyzer_api_base: http://presidio-analyzer:3000
presidio_anonymizer_api_base: http://presidio-anonymizer:3000
Reinicia, espera a que esté listo y ejecuta la misma solicitud:
San Francisco
Puedes ejecutar Presidio junto a una puerta de enlace, enmascarar solicitudes en el momento del registro sin
tocar la solicitud en vivo, y distinguir las dos direcciones con
presidio_filter_scope.
Demostrando correctamente
Una respuesta correcta podría ser una suposición afortunada — "San Francisco" es un predeterminado plausible
para una pregunta de código de área enmascarado, y temperature: 0 hace que una suposición se repita
tan confiablemente como una respuesta real. Así que pregunta sobre varios códigos de área que el modelo no
puede engañar:
KEY=$(grep -m1 LITELLM_MASTER_KEY ~/llm-gateway/.env | cut -d= -f2- | tr -d '"')
for ac in 907 808 216 505; do
printf "área %s -> " "$ac"
curl -s localhost:4000/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d "{\"model\":\"qwen-mid\",\"temperature\":0,\"messages\":[{\"role\":\"user\",\"content\":\"¿A qué estado de EE. UU. pertenece el código de área de este número de teléfono: ($ac) 555-0142? Responde solo con el nombre del estado.\"}]}" \
| python3 -c "import sys,json; print(' '.join(json.load(sys.stdin)['choices'][0]['message']['content'].split()))"
done
área 907 -> Alaska
área 808 -> Hawaii
área 216 -> Ohio
área 505 -> Nuevo México

Cuatro de cuatro. El modelo está leyendo los dígitos, así que la solicitud le está llegando
sin enmascarar. La colapsación de espacios en blanco en ese comando no es una tontería cosmética — qwen-mid es
un modelo de razonamiento y antepone sus respuestas con saltos de línea, lo suficientemente inconsistentes
que .strip() solo deja una salida irregular.
¿Por qué no simplemente enmascarar antes del modelo?
Es la pregunta obvia, y la respuesta obvia es incorrecta. mode: pre_call
enmascara la solicitud antes de que el modelo la vea — lo que suena como el arreglo más seguro posible.
Cambia la única línea y descubre:
mode: pre_call
Reinicia, luego pregunta la misma cosa:
KEY=$(grep -m1 LITELLM_MASTER_KEY ~/llm-gateway/.env | cut -d= -f2- | tr -d '"') && \
curl -s localhost:4000/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"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."}]}' \
| python3 -c "import sys,json; print(' '.join(json.load(sys.stdin)['choices'][0]['message']['content'].split()))"
Para determinar el estado de un código de área de un número de teléfono dado, necesitaría saber
el código de área específico del número de teléfono proporcionado. Por favor, proporciona el código de área
o el número de teléfono completo para que pueda identificar el estado correspondiente.
La redacción exacta se mueve entre ejecuciones incluso a temperature: 0 — el punto
es que cada versión de ella te pide el número que ya enviaste.

El modelo está pidiendo educadamente el número que acabas de darle. Recibió
<PHONE_NUMBER> y no hay nada en un marcador de posición para razonar. Nada falló, nada
dio error — simplemente obtuviste una respuesta inútil, que es el modo de fallo que es más difícil de notar en producción.
qwen-mid se comportó peor en mis ejecuciones: la misma solicitud se agotó dos veces a 40 segundos con max_tokens: 60, donde respondió en menos de un segundo con entrada sin enmascarar. No reclamaría un mecanismo de dos muestras, pero si estás
dirigiéndote a un modelo de razonamiento, mide esto antes de confiar en él.
Así que las tres ubicaciones, la misma pregunta cada vez:
| Configuración | El modelo recibe | Resultado |
|---|---|---|
mode: pre_call | <PHONE_NUMBER> | te pide el número que se le dio |
mode: logging_only, alcance dejado por defecto | dígitos reales | respuesta correcta, devuelta como <LOCATION> |
mode: logging_only, presidio_filter_scope: input | dígitos reales | San Francisco |
Solo el tercero te da una aplicación funcional. Pon mode de vuelta a
logging_only antes de continuar.
Lo que esto te da, y lo que no
Ahora tienes solicitudes que llegan a los modelos intactas, con el enmascaramiento de PII adjunto al camino de registro en lugar del camino de solicitud — y a una configuración de distancia de enmascarar silenciosamente las respuestas de tus usuarios.
Una cosa que esta solución no cubre, y es importante: el alcance input enmascara solo la solicitud.
Lo que sea que el modelo diga se registra tal cual — y un modelo preguntado sobre un cliente repetirá el nombre de ese cliente. Así que la PII que eliminaste
de la solicitud reaparece en la finalización. Ampliar el alcance a both cierra
ese agujero y reintroduce la corrupción de respuesta anterior; no hay configuración que haga ambas.
La Parte 4 mide la filtración
y construye la pieza que falta en async_logging_hook, el callback documentado
que se ejecuta después de que se ha enviado la respuesta.
Siendo directo sobre el límite: la copia enmascarada aterriza donde sea que tus callbacks de registro la envíen. La propia tabla LiteLLM_SpendLogs de la puerta de enlace no es una de
esas — messages se mantuvo en {} en cada configuración que probé, incluso con
store_prompts_in_spend_logs configurado tanto en config.yaml como como variable de entorno. La documentación apunta a Langfuse y destinos similares, que es
donde se ve la próxima publicación. Hasta que hayas conectado uno y visto una carga enmascarada en él, trata la mitad de enmascaramiento como no probada en tu propio despliegue.
Dos cosas más que vale la pena saber antes de que esto se ponga frente a tráfico real. La
guardia predetermina fail_on_error: true con unreachable_fallback: fail_closed, así que si Presidio se cae tus solicitudes fallan en lugar de pasar
sin enmascarar — el valor predeterminado correcto, pero planifica para ello. Y cada llamada a Presidio agrega
latencia al camino de registro; el método de temporización de la Parte 2 se aplica aquí también.
Solución de problemas
docker compose ps muestra dos contenedores, no cuatro
El archivo de compose no se guardó. docker compose config --services lee lo que está
en disco — si Presidio falta allí, la edición no se realizó.
curl: archivo ejecutable no encontrado en $PATH
La imagen de LiteLLM no tiene curl. Sondea con la línea de un solo comando python -c anterior.
Nombre o servicio no conocido al alcanzar presidio-analyzer
Las dos pilas están en diferentes redes de Docker. Ambos conjuntos de servicios deben estar en el mismo proyecto de compose, o compartir una red externa.
El inicio falla con ValidationError: mode Field required
mode es obligatorio en una guardia. Omitirlo sale del proxy al inicio —
Application startup failed. Exiting. No puedes expresar solo registro dejando
fuera mode.
El modelo responde correctamente pero la respuesta es <LOCATION> o <PHONE_NUMBER>
presidio_filter_scope está en su predeterminado both. Establecerlo en input.
Un número de teléfono no se está enmascarando
Verifica la puntuación. 555-0142 no se detecta en absoluto; (415) 555-0142 puntúa 0.4.
Usa presidio_score_thresholds para establecer un umbral por entidad.
Los cambios en config.yaml parecen no tener efecto
Edita el archivo, luego reinicia — en ese orden. Y un cd de un paso anterior
se lleva consigo, así que verifica en qué directorio estás antes de ejecutar docker compose.
Los cambios en .env son ignorados
docker compose restart no recarga el entorno. Usa
docker compose up -d --force-recreate litellm.
Qué sigue
El enmascaramiento está configurado, pero aún no has visto aterrizar una carga enmascarada
en ninguna parte. La próxima publicación conecta un destino de registro real a la puerta de enlace y busca
<PERSON> en un rastreo — cerrando el bucle que esta deja deliberadamente abierto —
y luego se dirige a presupuestos y lo que realmente cuesta todo el arreglo.
Lectura adicional
- Enmascaramiento de PII, PHI — Presidio — la referencia de guardia, incluyendo
presidio_filter_scopeypresidio_score_thresholds - Registro, Alertas, Métricas — los destinos a los que puede ir una copia enmascarada
- Presidio — el analizador y el anonimizador
- Enmascara tus registros, no tus solicitudes — por qué el enmascaramiento pertenece al camino de almacenamiento
- Parte 1 — desplegar una puerta de enlace LLM en una instancia WEC
- Parte 2 — enrutamiento de complejidad y lo que cuesta
