Token Classification and Span Extraction¶
mlxcel runs two kinds of per-token output model offline, each behind its own subcommand:
| Shape | Subcommand | Labels | Output |
|---|---|---|---|
| Token classification | mlxcel tag | fixed by the checkpoint (id2label, BIO or BIOES) | tagged character spans and a redacted copy of the text |
| Span extraction | mlxcel extract | chosen per request (--labels, --labels-json, --classify) | label-keyed span lists, or one class per task |
The two shapes are separate because they take different inputs: a fixed-label classifier handed request labels would have to ignore them silently. Passing a checkpoint to the other subcommand fails with a message naming the right one.
Neither shape is served over HTTP. mlx-vlm, the reference implementation, defines no route for them, so there is no upstream schema to match.
Supported Families¶
| Family | Detection | Kind | Decode |
|---|---|---|---|
| BERT / XLM-RoBERTa token classifier | model_type bert, xlm-roberta or xlm_roberta, architectures[0] ending in ForTokenClassification, non-empty id2label | bert_token_classifier | per-token argmax |
| OpenAI Privacy Filter | model_type openai_privacy_filter, non-empty id2label | openai_privacy_filter | constrained BIOES Viterbi (default), per-token argmax with --argmax |
| GLiNER2.5 span extractor | model_type gliner2 or gliner2_5, or an architecture naming Gliner2ForSchemaExtraction or BoundaryExtractor | gliner2_5 | sigmoid span scores over a shared candidate pool, overlaps resolved per label |
Detection reads config.json only and is separate from the generation, embedding, and rerank dispatch. mlxcel embed still loads a BertForTokenClassification checkpoint as a BERT embedder, and mlxcel generate refuses a token classifier with a message naming mlxcel tag.
mlxcel tag¶
mlxcel tag -m nativ-community/rampart-mlx-fp16 --keep CITY,STATE,ZIP_CODE \
"I'm Sarah Connor, 1984 Cyberdyne Ave, Los Angeles CA 90012, sarah@sky.net"
mlxcel tag -m mlx-community/openai-privacy-filter-bf16 "Alice Smith can be reached at alice@example.com."
| Flag | Description |
|---|---|
-m, --model <PATH_OR_REPO_ID> | Checkpoint directory or HuggingFace repo-id, resolved and downloaded like mlxcel generate -m. |
TEXT | The text to tag. - reads it from stdin. |
--argmax | Force per-token decoding on a checkpoint whose default decoder is Viterbi. |
--replacement <STR> | The string every redacted span becomes, instead of its placeholder. |
--keep <L1,L2> | Entity labels reported in spans but left in redacted_text. Unknown labels are ignored. |
--operating-point <NAME> | Calibration operating point of a Viterbi-decoded checkpoint (default default). |
--models-dir <PATH> | Model-store root for repo-id resolution. |
The JSON document goes to stdout and a one-line summary (span count, kind, model_type) goes to stderr:
{
"text": "Alice Smith can be reached at alice@example.com.",
"spans": [
{"label": "private_person", "start": 0, "end": 11, "text": "Alice Smith", "placeholder": "<PRIVATE_PERSON>"},
{"label": "private_email", "start": 30, "end": 47, "text": "alice@example.com", "placeholder": "<PRIVATE_EMAIL>"}
],
"redacted_text": "<PRIVATE_PERSON> can be reached at <PRIVATE_EMAIL>."
}
Only redacted_text is redacted. text and every spans[].text still hold the original values, so pass on redacted_text alone when the output leaves a trusted context.
Long text is never truncated. The tokenizer must report character offsets, and the ids are split into windows of the model's context size, wrapped in the checkpoint's special tokens, scored, decoded, and concatenated. Character ranges come from the offsets with surrounding whitespace trimmed, and a word split across a window boundary still forms one span. Offsets are Unicode scalar indices into text, matching Python string indexing.
A placeholder is < plus the label upper-cased, with every run of non-alphanumeric characters replaced by _, plus >. --replacement overrides it.
OpenAI Privacy Filter¶
openai/privacy-filter is a 1.4B-parameter bidirectional sparse-MoE encoder that tags PII with BIOES labels. mlxcel runs it through the same pipeline as mlxcel tag with the upstream settings: 128000-token windows, no special tokens, and no word grouping.
The default decoder is a constrained Viterbi path over the BIOES lattice, which is why the output never contains a label path the span builder has to repair. --argmax switches to independent per-token decoding.
viterbi_calibration.json in the checkpoint may carry named operating points, each a set of six additive log-domain biases on the lattice transitions. --operating-point selects one; the published checkpoints ship a single default point with every bias at zero, and the flag is refused for checkpoints that do not decode with Viterbi.
mlxcel extract¶
mlxcel extract -m fastino/gliner2.5-small-v1 --labels company,person,location \
--include-confidence --include-spans "Apple hired Sam in New York."
mlxcel extract -m fastino/gliner2.5-small-v1 \
--classify '{"sentiment": ["positive", "neutral", "negative"]}' "The launch was excellent."
Exactly one of --labels, --labels-json, and --classify is required.
| Flag | Default | Description |
|---|---|---|
-m, --model <PATH_OR_REPO_ID> | required | GLiNER2.5 checkpoint. |
--labels <L1,L2> | - | Comma-separated entity labels. |
--labels-json <JSON> | - | Label map, where a value is a description or null. |
--classify <JSON> | - | Classification tasks, as {"<task>": [labels]} or an object with labels, prompt, label_descriptions, multi_label, and threshold. |
--threshold <F> | 0.5 | Minimum entity score, and the default multi-label threshold. |
--include-confidence | off | Add each span's confidence to the output. |
--include-spans | off | Add each span's start and end offsets. |
--max-len <N> | checkpoint value | GLiNER2.5 only. Longest text window, an integer in 1..=32768. |
--word-splitter <whitespace\|char> | whitespace | GLiNER2.5 only. char splits one word per non-space character, for scripts without word spacing such as CJK. |
--models-dir <PATH> | global store | Model-store root for repo-id resolution (env MLXCEL_MODELS_DIR). |
Entity output is {"entities": {"<label>": [...]}} with one key per requested label, in request order. --classify prints {"<task>": "<label>"}, or a label list for a multi-label task.