GLiNER made entity extraction feel like a small-model job.
The task is narrower than asking a large model for JSON. Find the span, label it, keep the offsets, and let the next stage decide what to do.
That output shape is the useful part. A result can carry text, label, span boundaries, and score:
"sk-123..." -> api_key
"ghp_xxx..." -> token
"-----BEGIN PRIVATE KEY-----" -> private_key
Those spans can feed redaction, review, or deterministic validation. They are evidence, not a complete policy.
A local extraction pass can stay explicit:
from gliner import GLiNER
model = GLiNER.from_pretrained("urchade/gliner_medium-v2.1")
labels = ["api_key", "token", "password", "private_key"]
spans = model.predict_entities(note_text, labels, threshold=0.35)
high_risk = [span for span in spans if span["score"] >= 0.8]
needs_review = [span for span in spans if span["score"] < 0.8]
The model finds a span in the text. The surrounding system keeps the source sentence, applies the threshold, and decides whether to redact automatically.
The route is the product:
high confidence -> redact automatically
medium confidence -> show in review
low confidence -> ignore or run deterministic check
A weak “password” match should not disappear silently. It can be shown as a possible secret so a person can distinguish a real credential from a harmless sentence.
Thresholds are an interface choice. Lower them and the review queue fills with noise. Raise them and the model misses messy cases. Report precision, recall, and the labels that need a different threshold.
A classifier-style extraction step is easier to reason about when the labels, score, and abstention behavior are visible. The model is one bounded stage, not the whole workflow. For this job, that is the strength.
Related posts

About Jeremy London
Engineering leader and builder in Denver. I write about AI platforms, agents, security, reliability, homelab infrastructure, and the parts of engineering work that have to survive production.