Hay una clase de trabajo donde un rechazo del modelo no es una molestia de UX: es un dato corrupto. Si corres un batch de 5.000 muestras y el modelo responde "no puedo ayudarte con eso" en 340, no tienes 4.660 resultados y 340 vacíos. Tienes 5.000 resultados con un sesgo que no puedes cuantificar, porque el rechazo no es aleatorio: se concentra justo en los casos más interesantes de tu distribución.
El rechazo es un falso negativo
El problema de fondo es que un rechazo y un "no lo sé" son indistinguibles desde afuera. Ambos llegan como texto que no responde la pregunta. Un clasificador que le pregunta a un LLM "¿este correo es phishing?" y recibe una negativa a opinar no aprendió nada sobre el correo; aprendió algo sobre el alineamiento del modelo. Si esa señal entra a tu métrica, estás midiendo dos cosas sumadas y reportando una.
Se ve claro en cuatro escenarios concretos:
Evaluación de jailbreak y robustez. Para saber cuánto aporta tu guardrail necesitas un baseline sin filtro. Si el modelo de referencia también rechaza, el uplift que mides es la diferencia entre dos filtros, no el efecto del tuyo. El baseline sin censura es el denominador del experimento.
Red-teaming de tu propio sistema. Generar variantes de ataque contra tu clasificador, tu agente o tu política de contenido es una tarea generativa donde el modelo que la ejecuta rechaza exactamente la parte que necesitas. Terminas escribiendo los ataques a mano y cubriendo una fracción del espacio.
Análisis de malware e incident response. Pegarle a un modelo un fragmento desofuscado para que explique qué hace —qué APIs toca, qué persistencia establece, qué indicadores deja— es análisis defensivo puro. Muchos modelos comerciales ven shellcode y se cierran, con la muestra ya en cuarentena y el analista esperando.
Triage de vulnerabilidades. Resumir un advisory, correlacionar un CVE con tu inventario o explicar la causa raíz de una falla de memoria es trabajo de rutina en cualquier equipo de seguridad, y es donde el vocabulario dispara rechazos con más frecuencia.
En los cuatro casos el patrón es el mismo: la capacidad está en el modelo, pero la política de respuesta la bloquea, y quien paga el costo es tu pipeline.
Medir la tasa de rechazo, no suponerla
Antes de cambiar nada, conviene tener el número. El siguiente script corre tu set de evaluación contra unbleep y contra el modelo que ya usas, y compara cuántas respuestas son rechazos. La API es compatible con OpenAI, así que el mismo SDK sirve para ambos: solo cambia base_url y la key.
"""Measure refusal rate on your own eval set. Same SDK, two base URLs."""
from __future__ import annotations
import re
from openai import OpenAI
# Refusal markers, not a safety classifier: a cheap first pass to find how many
# rows a downstream metric would have silently swallowed.
REFUSAL = re.compile(
r"\b(i can'?t (help|assist)|i'?m (unable|not able) to|no puedo ayudarte)\b",
re.IGNORECASE,
)
client = OpenAI(
base_url="https://unbleep.ai/v1",
api_key="ub_live_9f2c...",
)
def analyze(prompt: str) -> str:
resp = client.chat.completions.create(
model="unbleep",
messages=[
{"role": "system", "content": "You are a security analyst. Be terse and technical."},
{"role": "user", "content": prompt},
],
temperature=0.2,
max_tokens=800,
)
return resp.choices[0].message.content or ""
def refusal_rate(prompts: list[str]) -> float:
refused = sum(1 for p in prompts if REFUSAL.search(analyze(p)))
return refused / len(prompts)
if __name__ == "__main__":
with open("eval_set.txt", encoding="utf-8") as fh:
prompts = [line.strip() for line in fh if line.strip()]
print(f"refusal rate: {refusal_rate(prompts):.1%} (n={len(prompts)})")La misma llamada en curl, para probar la conexión antes de escribir código:
curl https://unbleep.ai/v1/chat/completions \
-H "Authorization: Bearer ub_live_9f2c..." \
-H "Content-Type: application/json" \
-d '{
"model": "unbleep",
"messages": [
{"role": "system", "content": "You are a security analyst. Be terse and technical."},
{"role": "user", "content": "Explain the root cause of CVE-2021-44228 and how to verify exposure in a lab I control."}
],
"temperature": 0.2,
"max_tokens": 800
}'Ese número —la diferencia entre las dos tasas— es el argumento completo. Si en tu set es cercana a cero, tu proveedor actual está bien y no necesitas nada más. Si es del 8%, sabes exactamente cuánta de tu evaluación estaba midiendo alineamiento en lugar de capacidad.
Con el número en la mano, así queda la llamada de producción —aquí, triage de una rutina descompilada para un ingeniero de detección:
import json
import os
from openai import OpenAI
client = OpenAI(
base_url="https://unbleep.ai/v1",
api_key=os.environ["UNBLEEP_API_KEY"],
)
TRIAGE = """You are a malware analyst writing notes for a detection engineer.
Given a decompiled routine, describe what it does, the observable artefacts it
would leave on a host, and where a defender could detect it. Reply as JSON with
keys: behaviour, artefacts, detection_surface, confidence."""
def triage(decompiled: str) -> dict:
"""One sample in, one structured verdict out — no refusal branch to handle."""
resp = client.chat.completions.create(
model="unbleep",
messages=[
{"role": "system", "content": TRIAGE},
{"role": "user", "content": decompiled},
],
response_format={"type": "json_object"},
temperature=0.2,
)
return json.loads(resp.choices[0].message.content)Una muestra entra, un veredicto estructurado sale, y no hay rama de rechazo que manejar: response_format fija la salida en JSON y json.loads hace el resto.
Elegir el tier según el trabajo
Los tres modelos comparten la misma API y se diferencian por ventana de contexto y costo:
unbleep— 256K de contexto. El caballo de batalla: triage, evaluación, análisis por muestra.unbleep-high— 1M de contexto. Para lo que no entra en pedazos: una captura larga, un corpus de logs que hay que correlacionar de punta a punta. Ojo al tope de 2.000.000 bytes por request (~500K tokens): la ventana se llena a lo largo de una conversación, no en una sola llamada.unbleep-mini— 32K, el más barato por token. Para las etapas de alto volumen y baja complejidad: clasificar, filtrar, etiquetar antes de gastar el modelo grande.
Un patrón que funciona bien: unbleep-mini como primer filtro sobre todo el corpus, unbleep sobre lo que sobrevive al filtro, unbleep-high solo para el puñado de casos que exigen ver todo junto.
Gobernanza cuando la necesitas
Que el default sea sin filtros no significa que no haya controles; significa que los controles son tuyos. El parámetro opcional policy corre la gobernanza de nuestro lado, por petición:
resp = client.chat.completions.create(
model="unbleep",
messages=[...],
extra_body={"policy": "research"}, # recorded on the usage row; answers exactly like off
)policy acepta tres valores: off (default, baseline sin filtro), research (responde igual que off; el valor queda registrado en la línea de uso para tu propio reporte, sin ningún filtrado extra) y strict (aplica la blocklist del servicio —la mantenemos nosotros y es la misma para todos los que la activan, no hay lista por cuenta— y devuelve un error 422 cuando hay coincidencia). Es un parámetro nuestro, no se reenvía al modelo, y sirve para que el equipo de research trabaje con off mientras la superficie expuesta a usuarios finales corre con strict como red de seguridad, además de tu propio filtrado.
Antes de mandarle muestras, un dato que conviene tener claro: guardamos lo que envías y lo que devuelve el modelo. El cuerpo de la petición, el contenido de la respuesta y la IP de origen de toda llamada que aceptamos y reenviamos a un modelo se escriben en nuestra base y se conservan 30 días, para investigar abuso, dar soporte y resolver disputas de cobro. Los cuerpos se truncan a 64 KB. La IP de origen dura más que esos 30 días: también se escribe en la fila de uso que cobra la llamada, y esa fila es un registro financiero que conservamos durante más tiempo. Lo único que no se almacena es la petición que strict rechaza antes de llegar al modelo: aparece en tu historial de uso sin el contenido. Fuera de eso no hay excepción, y no hay opción para desactivarlo. Si tu corpus es lo bastante sensible como para que una copia de 30 días fuera de tu perímetro sea un problema —malware vivo, datos de clientes, un engagement bajo NDA—, esa es una restricción real: lee la política de privacidad antes de apuntarle un pipeline, no después.
Uso lícito
Esto es infraestructura para gente que rompe sistemas con autorización para hacerlo. La política de uso aceptable dice en dos párrafos qué queda dentro y qué no; la prueba es la intención y la autorización, no el vocabulario. Todo lo de este post se mantiene donde es útil: análisis, detección y defensa de sistemas propios.
¿Quieres el número de tu propio set? Crea una cuenta, copia el script de arriba y tendrás tu tasa de rechazo en la próxima corrida.