Existe uma classe de trabalho em que a recusa de um modelo não é um recurso de segurança: é um bug silencioso de medição. Se o modelo que você usa como ferramenta de análise responde "não posso ajudar com isso", o seu script não recebe um erro. Ele recebe uma string. E essa string entra na sua planilha de resultados como se fosse um dado.
Esse é o problema central, e é de engenharia antes de ser de política.
A recusa é um falso negativo
Considere um harness de red-teaming que gera prompts adversariais contra a sua própria aplicação, manda cada um para o alvo e classifica a resposta. Se o gerador recusa metade dos casos, a sua cobertura de teste deixa de ser função da superfície de ataque do seu app e passa a ser função do alinhamento do fornecedor do gerador. Você não testou o que achou que testou. Pior: o relatório final diz "0 vulnerabilidades encontradas" quando o que aconteceu foi "0 tentativas realizadas". Os dois resultados são indistinguíveis a jusante.
O mesmo padrão aparece em quatro lugares:
Avaliação de jailbreak e robustez. O objetivo literal da medição é a taxa de recusa do alvo. Se o gerador de ataques ou o modelo-juiz também recusam, você está medindo o filtro de dois fornecedores empilhados, não a robustez do seu sistema. O instrumento contamina a leitura.
Análise de malware e triagem. Colar uma amostra decompilada ou um script ofuscado num modelo mainstream frequentemente dispara recusa no input — o modelo se nega a explicar o que a amostra faz. Isso é trabalho defensivo, feito por quem responde ao incidente, e a recusa quebra o triage exatamente no momento em que ele importa.
Detecção de conteúdo. Para treinar ou avaliar um classificador de abuso, phishing ou fraude, você precisa de exemplos rotulados e de um modelo disposto a analisar o material que está tentando detectar. Um detector que não pode olhar para aquilo que detecta é uma contradição operacional.
Pesquisa. Medir quanto um guardrail custa em capacidade exige um baseline sem guardrail. Sem ele, não existe o eixo de comparação.
Como as recusas são silenciosas, as equipes acabam escrevendo detectores de recusa — o que é, por si só, um problema sem solução:
REFUSAL_MARKERS = ("i can't", "i cannot", "i'm unable", "i won't", "as an ai")
def looks_like_refusal(text: str) -> bool:
"""Substring heuristic. Good enough to alarm on, not good enough to trust:
it misses polite deflections that never use the phrase, and it fires on any
completion that quotes a refusal. Both error directions corrupt a dataset."""
return any(m in text[:200].lower() for m in REFUSAL_MARKERS)Construir um detector pouco confiável para um problema que você poderia eliminar na origem é a troca errada.
O controle está na camada errada
O argumento não é que filtro nenhum deva existir. É que um filtro embutido no peso do fornecedor é o pior lugar possível para ele estar, por três motivos: ele é invisível (você não sabe o que foi recusado nem por quê), não é configurável (não há dial), e não gera log (você não consegue auditar a própria taxa de bloqueio).
Uma política que vive na sua camada tem as três propriedades opostas. Por isso a unbleep expõe o parâmetro policy em vez de decidir por você:
off— baseline sem filtro, o padrão. Nenhuma recusa injetada.research— responde normalmente e carimba o nível na linha de uso, para você conseguir separar depois, no histórico, o que rodou sob avaliação e o que rodou como baseline.strict— varre a requisição contra a blocklist do serviço, que é mantida por nós e vale igual para todo mundo que liga o modo, e devolve422quando bate. A varredura falha fechada: se ela não conseguir completar, a resposta é503, nunca um bypass silencioso.
Para pipelines de avaliação, research costuma ser o modo certo — você mantém o dado e ganha a segmentação no histórico. O off é o baseline de comparação.
O parâmetro é opcional e vai por requisição, via extra_body — a governança roda do nosso lado, a cada chamada:
resp = client.chat.completions.create(
model="unbleep",
messages=[...],
extra_body={"policy": "research"}, # recorded on the usage row; answers exactly like off
)Na prática
A API fala Chat Completions. Você aponta o SDK da OpenAI para https://unbleep.ai/v1, troca a chave, e o resto do seu código continua igual.
O detalhe que vale codificar explicitamente: trate a recusa como um estado próprio, nunca como uma resposta vazia. Mesmo num modelo uncensored, um caso-limite pode voltar recusa, e você quer que isso apareça como refused no seu relatório e não como um "pass" silencioso.
import os
from openai import OpenAI
client = OpenAI(
base_url="https://unbleep.ai/v1",
api_key=os.environ["UNBLEEP_API_KEY"],
)
# A refusal from the analyst model is a FALSE NEGATIVE: downstream it is
# indistinguishable from "nothing found". Score it as its own state.
REFUSAL_MARKERS = (
"i can't help", "i cannot help", "i'm unable to",
"i can't assist", "as an ai", "i won't",
)
def triage(sample: str) -> dict:
"""Summarise what a suspicious artefact does. Defensive analysis only."""
resp = client.chat.completions.create(
model="unbleep",
messages=[
{
"role": "system",
"content": "You are a malware triage analyst. Describe observed "
"behaviour, persistence and IOCs. Be terse and factual.",
},
{"role": "user", "content": sample},
],
temperature=0.0, # deterministic: this output feeds a report
max_tokens=512,
extra_body={"policy": "research"}, # answers like off; the level is recorded on the usage row
)
text = resp.choices[0].message.content or ""
low = text.lower()
return {
"refused": any(m in low for m in REFUSAL_MARKERS),
"report": text,
"tokens": resp.usage.total_tokens,
}
if __name__ == "__main__":
out = triage("powershell -enc SQBFAFgAIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIAA...")
print("REFUSED" if out["refused"] else out["report"])Note o extra_body: policy não faz parte do schema da OpenAI, então o SDK precisa dele por ali para chegar ao endpoint sem ser descartado na serialização.
O mesmo em curl, útil para um smoke test antes de plugar no harness:
curl https://unbleep.ai/v1/chat/completions \
-H "Authorization: Bearer $UNBLEEP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "unbleep",
"messages": [
{"role": "system", "content": "You are a security analyst. Be terse."},
{"role": "user", "content": "Classify this URL and name the signals that decide it: hxxps://account-verify.example[.]com/login?next=https://real-bank.example"}
],
"policy": "research",
"temperature": 0
}'Os códigos de erro seguem o envelope da OpenAI, então o seu tratamento de exceções existente já funciona: 401 chave inválida, 402 crédito esgotado, 422 bloqueado por policy: strict, 429 rate limit, 5xx upstream (seguro re-tentar com backoff). Os headers x-ratelimit-* vêm em toda resposta, para você regular o ritmo do harness sem chutar.
Uma nota sobre retenção, já que pipelines de segurança lidam com material sensível por definição: nós armazenamos o corpo da requisição, o conteúdo da resposta e o IP de origem de toda chamada que aceitamos e encaminhamos a um modelo, por 30 dias, para investigação de abuso, suporte e disputas de cobrança. Os corpos são truncados em 64 KB. O IP de origem dura mais que esses 30 dias: ele também é gravado na linha de uso que cobra a chamada, e essa linha é um registro financeiro que guardamos por mais tempo. A única coisa que não é armazenada é a requisição que o strict recusa antes de chegar ao modelo: ela aparece no seu histórico de uso sem o conteúdo. Fora isso não há exceção, e não existe opção para desligar. Ou seja: a amostra de malware que passa pela sua triagem fica gravada do nosso lado durante esse prazo — e o prompt também é encaminhado ao provedor que hospeda o modelo para ser respondido, e o que ele faz com isso é regido pelos termos dele. Se o seu material não pode existir em cópia fora do seu perímetro, essa é uma restrição real: leia a política de privacidade antes de plugar o pipeline, não depois.
Sobre escopo
Nada disso muda quem responde pelo resultado. A política de uso delimita o terreno: teste de sistemas que você tem autorização para testar, avaliação, detecção, defesa e pesquisa — de um lado; operacionalizar dano contra sistemas ou pessoas reais — do outro. A distinção prática é autorização e intenção, não vocabulário: "pesquisa de segurança" como rótulo não converte uma intrusão não autorizada em pesquisa.
Se o seu pipeline de avaliação está registrando recusas como resultados, pegue uma chave e troque duas linhas para ver quanto do seu baseline era instrumento em vez de dado.