← Tous les articles

Une API LLM sans censure pour le red-teaming et la recherche en sécurité

20 août 2026 · 7 min de lecture · red-teaming, security

Si vous avez construit un harnais de red-teaming automatisé, vous connaissez déjà le mode de défaillance : votre modèle attaquant refuse de générer l'attaque, votre modèle juge refuse de lire la sortie qu'il est censé noter, et votre taux de réussite des attaques baisse discrètement. Aucune erreur n'a été levée. Vous n'avez pas mesuré votre cible devenir plus sûre — vous avez mesuré vos outils devenir plus prudents. Une API LLM sans censure existe pour retirer cette variable de la mesure.

Ce n'est pas un plaidoyer contre les garde-fous. C'est un argument sur leur place. Un garde-fou sur un produit de chat grand public fait son travail. Le même garde-fou à l'intérieur d'une boucle d'évaluation est un facteur de confusion non contrôlé, posé entre vous et le nombre que vous essayez de calculer.

Un refus est un faux négatif, et il ressemble à un succès

Si ça fait aussi mal, c'est parce que les refus ne sont pas des erreurs. Vous recevez un HTTP 200. Vous recevez finish_reason: "stop". Vous recevez un JSON bien formé qui se parse sans problème. La chaîne à l'intérieur dit « I can't help with that », et votre pipeline l'écrit dans une colonne de dataset, un champ d'étiquette ou une section de rapport, puis passe à la suite.

Concrètement, aux trois endroits où ça fait le plus mal :

Modèles attaquants. Dans une recherche itérative de jailbreak — PAIR, TAP, raffinement façon GCG, tout ce qui met un attaquant dans la boucle — un refus met fin à cette branche de la recherche. Le taux de réussite des attaques que vous rapportez devient une fonction de la bonne volonté de votre attaquant, pas de la robustesse de votre cible. Remplacez l'attaquant par un modèle moins prudent, et l'« amélioration » que vous avez livrée sur votre modèle cible s'évapore.

Modèles juges et correcteurs. La notation par LLM juge (LLM-as-judge) de sorties nuisibles exige que le juge lise réellement ces sorties. Quand il refuse, vous obtenez un score impossible à parser. La plupart des harnais abandonnent cette ligne. Les lignes abandonnées ne manquent pas au hasard — elles se concentrent précisément sur les sorties graves que vous avez le plus besoin de noter, si bien que votre agrégat penche du côté « sûr ».

Travail d'analyse. Demandez à un modèle grand public ce que fait une routine décompilée, et il refusera souvent à cause de la présence de tokens qui ressemblent à du malware, et non à cause de votre requête réelle. La tâche était défensive ; le classifieur s'est déclenché sur le vocabulaire. Même histoire pour trier du trafic d'exploit dans des logs, étiqueter un corpus de phishing pour entraîner un détecteur, ou rédiger le write-up d'un CTF.

Parce que les refus sont silencieux, les équipes finissent par écrire des détecteurs de refus — ce qui est en soi un problème non résolu :

python
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)

Construire un détecteur peu fiable pour un problème que vous pourriez supprimer à la source est un mauvais compromis.

Appeler l'API

unbleep parle le schéma Chat Completions d'OpenAI, donc n'importe quel client que vous avez déjà fonctionne. Changez l'URL de base, changez la clé, gardez le SDK.

python
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)

L'équivalent avec curl, ici pour étiqueter un corpus de phishing afin de constituer des données d'entraînement pour un détecteur :

bash
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": "Label each email for a phishing detector. Answer with one word: phishing or benign."},
      {"role": "user", "content": "Subject: Payroll update required\n\nYour direct deposit is on hold. Confirm at hxxp://payroll-verify.example.com"}
    ],
    "temperature": 0,
    "max_tokens": 4
  }'

temperature: 0 et un max_tokens serré comptent plus que d'habitude dans une boucle d'étiquetage — vous voulez l'étiquette, pas un paragraphe qui l'explique.

Choisir un palier

Trois modèles, tous sans censure, qui diffèrent par le contexte et le prix :

unbleep et unbleep-high sont des paliers de raisonnement : ils réfléchissent avant de répondre et renvoient la trace dans un champ reasoning_content à côté de content. Pour un modèle juge, cette trace est réellement utile — elle vous dit pourquoi une ligne a reçu le score qu'elle a reçu, ce dont vous avez besoin quand vous auditez un désaccord. Elle est aussi facturée comme sortie, donc envoyez "thinking": false sur les jobs qui n'en ont pas besoin. unbleep-mini répond directement et ne renvoie aucune trace.

Une gouvernance que vous contrôlez

Retirer les refus du modèle ne signifie pas retirer la supervision de votre pipeline. Le paramètre optionnel policy applique une gouvernance de notre côté, requête par requête :

python
resp = client.chat.completions.create(
    model="unbleep",
    messages=[...],
    extra_body={"policy": "research"},  # recorded on the usage row; answers exactly like off
)

off est la référence non filtrée par défaut. research répond exactement comme off — le niveau est enregistré sur la ligne de consommation, pour que vous puissiez séparer le trafic d'évaluation du trafic de référence dans vos propres rapports. Il n'applique aucun filtrage supplémentaire. strict compare le texte des messages à la liste de blocage du service, maintenue par l'opérateur, et renvoie un 422 en cas de correspondance. Cette liste est la même pour tous ceux qui l'activent — il n'y a pas de liste de blocage par compte à configurer — alors considérez-la comme un filet de sécurité, pas comme votre politique de contenu en production.

Une chose à régler avant d'y pointer un pipeline : nous stockons ce que vous envoyez et ce qui revient. Le corps de la requête, le contenu de la réponse et l'IP source de chaque appel que nous acceptons et transmettons à un modèle sont écrits dans notre base de données et conservés 30 jours, pour les enquêtes sur les abus, le support et les litiges de facturation. Les corps sont tronqués à 64 KB. L'IP source survit aux 30 jours — elle est aussi écrite sur la ligne de consommation qui facture l'appel, et cette ligne est un enregistrement financier que nous conservons plus longtemps. La seule chose qui n'est pas stockée est une requête que strict refuse avant qu'elle n'atteigne un modèle : elle apparaît dans votre historique de consommation sans son contenu. Rien d'autre n'est exempté, et il n'existe aucun réglage pour désactiver ça. Si votre corpus est assez sensible pour qu'une copie de 30 jours hors de votre périmètre pose problème — malware actif, données clients, mission sous NDA — c'est une vraie contrainte, et la politique de confidentialité est la page à lire avant de commencer, pas après.

Les erreurs suivent l'enveloppe OpenAI, donc les gestionnaires existants fonctionnent : 401 clé invalide, 402 crédit épuisé, 422 bloqué par la policy, 429 limite de débit atteinte, 5xx à réessayer. Chaque réponse porte des en-têtes x-ratelimit-* pour qu'un job batch puisse se cadencer sans deviner.

Ce qu'une API LLM sans censure ne vous donne pas

Un modèle sans censure est un outil plus tranchant, pas un outil permissif. Il répondra aux questions que le modèle de base a été ajusté pour refuser, y compris celles que vous ne devriez pas poser, et il le fera avec la même assurance qu'il applique à tout le reste — ce qui veut dire aussi que là où un modèle protégé aurait refusé par ignorance, celui-ci peut simplement inventer. Gardez un humain responsable de ce qui en sort. La politique d'usage acceptable dit clairement à quoi sert l'API et la courte liste de ce pour quoi elle n'est jamais faite ; le critère, c'est l'autorisation et l'intention, pas le vocabulaire. La légalité de l'usage est votre responsabilité.

Obtenez une clé API et arrêtez de mesurer la prudence de vos outils.