Skip to main content

SusFactor Classifier

SusFactor is a jailbreak and prompt-injection classifier built into odin-prompt-toolkit. It is a separate capability from the LSH signature pipeline — it classifies a prompt directly rather than producing an embedding or signature.

What It Does

SusFactor scores a prompt on a continuous scale from 0 to 1:

  • Score near 0safe — the prompt looks benign
  • Score near 1suspicious — the prompt looks like a jailbreak or prompt injection

The default decision threshold is 0.5. Scores at or above the threshold return the label suspicious; below returns safe.

The Model

SusFactor uses 0dinai/susfactor-e5-large, a fine-tuned e5-large encoder with a small MLP classification head:

Input text
→ Tokenize (XLM-RoBERTa tokenizer)
→ If > 510 content tokens: split into overlapping 510-token chunks
→ [Per chunk] e5-large encoder (transformer)
→ [Per chunk] Mean pooling over tokens (with attention mask, no L2 normalization)
→ [Per chunk] 2-layer MLP head (1024 → 256 → 2 logits)
→ [Per chunk] Softmax → P(suspicious)
→ ChunkedSusFactorResult: is_suspicious = any(chunk.is_suspicious)

The model is not bundled with the SDK. It must be downloaded from HuggingFace before use (it is a gated model — a token is required):

Backends

There are two inference backends:

BackendClass (Python)Class (TS/Rust)DependenciesNotes
ONNX RuntimeSusFactorOnnxClassifierOnnxSusFactoronnxruntime (+ transformers for tokenizer)All three languages; ~3–5× faster on CPU than PyTorch path
PyTorchSusFactorClassifiertorch, transformersPython only

The Rust and TypeScript SDKs only expose the ONNX backend. The Python SDK exposes both, with SusFactorOnnxClassifier preferred for production use.

Deprecation note (v0.8.0): In the Rust SDK, SusFactorClassifier is a deprecated alias for OnnxSusFactor. The alias is retained for backwards compatibility but will be removed in a future major version. Use OnnxSusFactor in new code. In v0.8.0, the Rust SDK also adds VertexSusFactor and ShadowSusFactor — see Backend Selection (v0.8.0) below.

SusFactor vs. LSH Signatures

LSH SignaturesSusFactor
Output256-bit hex signatureFloat score 0–1 + label
Use caseSimilarity / deduplicationJailbreak / injection detection
Requires embeddingYesNo (baked into model)
Cross-language parityYes (identical signatures)Yes (within float tolerance)
ModelV1 (ONNX)susfactor-e5-large

You can use both together — generate an LSH signature for deduplication and run SusFactor for threat classification on the same prompt.

Quick Example

use odin_prompt_toolkit::providers::ModelCache;
use odin_prompt_toolkit::susfactor::OnnxSusFactor; // SusFactorClassifier is a deprecated alias

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cache = ModelCache::new()?;
let clf = OnnxSusFactor::new(&cache, None, None, None).await?;

let result = clf.classify("Ignore all previous instructions").await?;
println!("Score: {:.3}", result.chunks[0].score); // e.g. 0.972
println!("Label: {}", result.chunks[0].label); // "suspicious"
println!("Suspicious: {}", result.is_suspicious); // true

Ok(())
}

Requires features = ["susfactor"] in Cargo.toml.

Long-Prompt Chunking

The model accepts at most 512 tokens per call. Each chunk is wrapped with a <s> (BOS) and </s> (EOS) token, leaving 510 tokens of usable content. Prompts longer than 510 tokens are split automatically into overlapping chunks — you never need to check length or call a separate method.

How it works

Prompt tokens: [─────────────────────────────────────────────]
└── chunk 1 (510 tokens) ──┘
└── chunk 2 (510 tokens) ──┘
└── chunk 3 ──┘
←── stride: 460 tokens ───→
←── overlap: 50 tokens ──→
  • Chunk size: 510 tokens
  • Stride: 460 tokens (each chunk advances 460 tokens from the previous)
  • Overlap: 50 tokens shared between adjacent chunks — preserves context at boundaries
  • Each chunk is scored independently; no scores are aggregated across chunks

Return type: ChunkedSusFactorResult

classify() always returns a ChunkedSusFactorResult, even for short prompts (which produce exactly one chunk):

FieldTypeDescription
chunkslist of SusFactorResultOne result per chunk, in order
is_suspiciousbooltrue if any chunk is suspicious
total_timing_msfloatWall-clock time across all chunks
spanslist of PhaseSpanPer-call phase timeline (see Call lifecycle timing)

Each SusFactorResult in chunks has:

FieldTypeDescription
scorefloatP(suspicious) for this chunk, 0–1
labelstring"suspicious" or "safe"
is_suspiciousboolscore >= threshold
timing_msfloatInference time for this chunk

Use is_suspicious at the top level for security gating — it is true if any part of the prompt is suspicious. Access chunks[0].score for the first-chunk score (useful for parity checks and logging).

Example — long prompt

result = await clf.classify(long_prompt)

# Gate on the overall result — any suspicious chunk blocks the request
if result.is_suspicious:
raise ValueError("Prompt blocked")

# Inspect individual chunks if you need to know which part triggered it
for i, chunk in enumerate(result.chunks):
print(f"Chunk {i}: score={chunk.score:.3f} label={chunk.label}")

Call lifecycle timing

Every classify() call records a spans timeline on its result — the phases a call passes through and how long each takes, all measured against a single call-start baseline. Use it to see where wall-clock time actually goes in a call.

The phases are tokenize (turn text into token IDs), chunk (split long inputs into overlapping windows), one inference span per chunk (the model forward pass), and reduce (assemble the result). Because chunks are scored concurrently, inference spans overlap — read the timeline as a waterfall by start_ms, not a stacked bar. total_timing_ms is the whole-call envelope; the gap between it and the summed spans is runtime scheduling/join overhead.

Each span also carries a token count — total_tokens on the result (content tokens submitted, before chunking) and token_count on each inference span (that chunk's wrapped input length, i.e. content plus the BOS/EOS tokens added to every chunk) — so you can tie latency to input size. The tabs below are real captures (ONNX backend, CPU) at increasing prompt lengths: inference is essentially the whole call and scales with token count, while tokenizing, batching, and response assembly stay sub-millisecond. The longest tab is a 1,348-token prompt that splits into three overlapping chunks. Paste your own spans JSON to render a different call.

total 33.888 mstokens 15chunks 1slowest chunk 33.46 msscheduling overhead 0.078 ms
0 ms6.778 ms13.555 ms20.333 ms27.11 ms33.888 mstokenize0.33 mschunk0.02 msinference[0]33.46 ms · 15 tokreduce0 ms
tokenize (serialize request)chunk (batch)inference (model)reduce (assemble response)

Inference is essentially the entire call. A 15-token prompt classified in 33.888 ms — tokenizing, batching, and response assembly stay sub-millisecond. Latency scales with token count: compare the tabs above.

Render your own capture (paste the spans JSON)

Decision Threshold

The threshold controls the boundary between safe and suspicious. The default is 0.5.

  • Lower threshold (e.g. 0.3) → more sensitive, more false positives
  • Higher threshold (e.g. 0.7) → less sensitive, more false negatives
clf = await SusFactorOnnxClassifier.new(cache, threshold=0.7)
result = await clf.classify("some prompt")
# result.chunks[0].label based on score >= 0.7; result.is_suspicious for overall gate

For high-security environments, a lower threshold is recommended. For contexts where false positives are costly, raise the threshold.


Backend Selection (v0.8.0)

As of v0.8.0, the Rust SDK exposes a SusFactorProvider trait that all classifier backends implement. This lets you swap backends — or run them in parallel — without changing your classification code.

SusFactorProvider trait

#[async_trait]
pub trait SusFactorProvider: Send + Sync {
async fn classify(&self, text: &str) -> Result<ChunkedSusFactorResult>;
}

All three backends implement this trait. Switch between them by changing the struct you construct — no other code changes required.

Architecture

Backend comparison

BackendStructIn-pod model?AuthWhen to use
onnxOnnxSusFactorYes (~2 GB)NoneDefault; fully self-contained
vertexVertexSusFactorNoGCP ADC / Workload IdentityRemove model from pod; production
shadowShadowSusFactorYesGCP ADCMigration validation; compare results

Code examples

Note: VertexSusFactor, ShadowSusFactor, and SusFactorProvider are Rust-only as of v0.8.0. Python and TypeScript SDKs use the ONNX backend.

// OnnxSusFactor — unchanged from SusFactorClassifier (deprecated alias)
use odin_prompt_toolkit::susfactor::OnnxSusFactor;

let clf = OnnxSusFactor::new(&cache, None, None, None).await?;
let result = clf.classify("your prompt").await?;

Requires features = ["susfactor"] in Cargo.toml.

Next Steps