code-daemon-ner-v1
A small token-classification (BIO) model that finds software-engineering entities in prose β the sentences of a README, a design doc, an issue thread, a commit message. It ships with the UltraCode MCP server as a TensorRT / OpenVINO engine and feeds the documentation knowledge graph: the spans it emits become the concept nodes that link a paragraph of prose to the code entities it talks about.
This is prose NER, not code parsing. Identifiers inside source files are extracted by
tree-sitter, exactly and for free. What tree-sitter cannot do is read the sentence "the IVF index
rescoring path in faiss_index.zig regressed nDCG@10 by 4 points" and tell you that
IVF is an algorithm, faiss_index.zig a file path, and nDCG@10 a metric. That is this model's job.
- ~117M params β XLM-RoBERTa 12 layers / 384 hidden, 250k multilingual SentencePiece vocab (the embedding table dominates the parameter count).
- 2-input ONNX (
input_ids,attention_mask; notoken_type_ids) βlogits [batch, seq, 17]. - Max sequence 128 tokens β one or two sentences, which is the unit the daemon chunks prose into.
- Bilingual by construction (English + Russian docs), inherited from the XLM-R backbone.
What this model is for. Entity-level Core F1 is 0.43. That is a useful recall-oriented candidate generator for a knowledge graph, and it is not a labeller to trust unattended. The Evaluation section has the per-type numbers, including the two types that are weak and why.
Label set β 8 entity types, 17 BIO classes
Split into a Core tier (what the knowledge graph links on) and a Soft tier (useful, tolerated at lower precision). Both are scored separately.
| Tier | Type | What it marks | Example span |
|---|---|---|---|
| Core | component |
a named part of a system, ours or someone's β including model names | DocStore, mMiniLMv2 |
| Core | api_endpoint |
a callable surface: function, method, route, agent tool name | semantic_search, POST /v1/messages |
| Core | file_path |
a path or a filename | src/semantic/faiss_index.zig, config.toml |
| Core | tool |
an external executable, runtime, service, language or hardware you invoke | TensorRT, rclone, SQL, Tesla T4 |
| Soft | algorithm |
a named method or procedure | BM25, beam search, Louvain |
| Soft | data_structure |
a named container or layout | CSR, Prolly tree, arena |
| Soft | config |
a knob, flag, env var or setting | journal_mode=MEMORY, --epochs |
| Soft | metric |
a measurement or its unit | nDCG@10, macro-F1, p95 latency |
The component / tool / api_endpoint boundary costs annotators the most, so it is fixed by rule
rather than by feel, and the rules are applied in order β that ordering is what resolves names
fitting two types. A model is a component (a part of a system); an engine, runtime, language or
piece of hardware is a tool (you invoke it); an agent tool name is an api_endpoint (a callable
surface, not a program). About a dozen names are genuinely polysemous and take their label from the
sentence.
How it was made
There is no off-the-shelf corpus for this label set, so the training labels were manufactured by weak supervision β several independent labellers vote per chunk and the votes are reconciled into a single BIO sequence β followed by supervised fine-tuning of the backbone and a temperature calibration pass.
The calibration is the part you need at inference: a single scalar, shipped as temperature.json
(T = 0.665). Divide the logits by it before the softmax if you consume probabilities rather
than argmax.
Evaluation is human-labelled and disjoint from the training labels β 1 500 hand-reviewed sentences, normalised so that no surface name carries two labels and no span overlaps another.
Evaluation
Measured on the 1 500-row human gold set, entity-level (exact span and type, seqeval convention), macro-averaged over the bare types β the metric NER results are normally reported in.
| Split | macro-F1 | micro-F1 |
|---|---|---|
Core (component, api_endpoint, file_path, tool) |
0.4264 | 0.4502 |
Soft (algorithm, data_structure, config, metric) |
0.2072 | 0.1910 |
Per type, with precision and recall, because they differ a lot and the difference is the story:
| Type | Tier | F1 | P | R |
|---|---|---|---|---|
api_endpoint |
Core | 0.521 | 0.409 | 0.720 |
file_path |
Core | 0.497 | 0.391 | 0.682 |
tool |
Core | 0.425 | 0.421 | 0.429 |
component |
Core | 0.263 | 0.218 | 0.330 |
metric |
Soft | 0.273 | 0.404 | 0.206 |
algorithm |
Soft | 0.239 | 0.193 | 0.312 |
data_structure |
Soft | 0.198 | 0.169 | 0.239 |
config |
Soft | 0.119 | 0.166 | 0.093 |
What this says, plainly:
- Recall runs well ahead of precision on the strongest types, and that is a property of the
training labels rather than of the model:
api_endpointandfile_pathare labelled in the training corpus at roughly 2.6Γ the density the human gold uses, and the model over-predicts them by about the same factor. Over-labelled in, over-predicted out. If you use it as a candidate generator and filter afterwards, this is the behaviour you want; as a final labeller it is not. componentis the weakest Core type (0.263). Unliketoolit is not under-labelled β its training density already exceeds the gold's β so density is not what limits it. It is genuinely the hardest of the four: what counts as a named part of a system is contextual in a way a path or a call is not.configis the weakest overall (0.119, recall 0.093). Its labels are inconsistent between the two halves of the training corpus β dense in documentation, nearly absent in Q&A β and averaging two incompatible labelling regimes yields noise. A known, unfixed defect.- Multi-token spans are weak. The
I-classes are near-empty in training, so most predictions are effectively single-token. Only ~4% of Core gold spans are multi-word, so this costs less than it sounds, but do not expect long names to come back whole.
For direction rather than level: the model this replaces scores 0.077 Core on the same ruler, and the previous build of this one scored 0.323. All three were measured identically.
Intended use
Extract engineering entities from documentation-style prose to build or enrich a knowledge graph, to tag issues, or to route search. Use it as a recall-oriented candidate generator behind a rule or human filter, not as a labeller you trust unattended.
Out of scope: extracting symbols from source code (use a parser), general-domain NER (use a general-domain model), and any high-stakes automatic decision.
import onnxruntime as ort, numpy as np, json
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained(".") # bundled XLM-R SentencePiece
labels = json.load(open("code-daemon-ner-v1_label_map.json"))["id2label"]
T = json.load(open("temperature.json"))["T"]
sess = ort.InferenceSession("model.onnx", providers=["CPUExecutionProvider"])
def tag(text, max_len=128):
enc = tok(text, return_tensors="np", truncation=True, max_length=max_len,
return_offsets_mapping=True, return_token_type_ids=False)
logits = sess.run(None, {"input_ids": enc["input_ids"].astype(np.int64),
"attention_mask": enc["attention_mask"].astype(np.int64)})[0][0]
probs = np.exp(logits / T) / np.exp(logits / T).sum(-1, keepdims=True) # calibrated
for (a, b), row in zip(enc["offset_mapping"][0], probs):
lab = labels[str(int(row.argmax()))]
if lab != "O" and b > a:
yield text[a:b], lab, float(row.max())
for span, label, p in tag("The IVF rescoring path in src/semantic/faiss_index.zig cost 4 nDCG@10 points."):
print(f"{span!r:40} {label:18} {p:.2f}")
The numbers above were measured at sequence length 128 with identifier pre-splitting (camelCase / snake_case split before tokenizing), which is how the model was trained and how it is deployed. Scoring it at a different sequence budget, or without the pre-split, gives a different and lower result.
What's in this repo
Flat layout, named per runtime Γ GPU arch Γ OS (single-profile β no length buckets):
- TensorRT
code-daemon-ner-v1_{win_x64,linux_x64}_trt11.0_sm_120.engineβ INT8 Q/DQ, built for sm_120 (RTX 50xx). A serialized engine is keyed on {GPU arch Γ OS Γ TensorRT version} with no compat fallback, so a machine on another architecture uses the OpenVINO or ONNX path. - OpenVINO
code-daemon-ner-v1_ov2026.3_{cpu,igpu_lnl}_int8_b32_s128.{xml,bin}and..._npu_int4_b8_s128.{xml,bin}β Intel CPU / iGPU / NPU. - Tokenizer β
sentencepiece.bpe.model(XLM-R SP; the daemon loads it natively) plustokenizer_config.json, which is whatAutoTokenizer.from_pretrainedreads. The fast tokenizer ships ascode-daemon-ner-v1_tokenizer.json. - Labels & calibration β
code-daemon-ner-v1_label_map.json(17 classes,label2id/id2label),temperature.json(a single scalar under the keyT),manifest.json(build metadata),eval_metrics.json(per-type and per-class F1). The label map and the fast tokenizer carry the model-id prefix because that is the name the daemon resolves; the snippet above uses the same names. - ONNX β
model.onnxFP32 dynamic-shape (the build source, and standaloneonnxruntime/optimumuse),model_static.onnx(seq pinned to 128, the engine-build input), andmodel_int8qdt.onnx(INT8 Q/DQ).
No MLX build: this model has no Apple-GPU consumer today.
License
Released under the MIT license.
| Source | Role | Note |
|---|---|---|
nreimers/mMiniLMv2-L12-H384-distilled-from-XLMR-Large |
backbone | a re-upload of Microsoft's mMiniLMv2 (microsoft/unilm, MIT); the re-upload repo itself declares no license |
Neither the training corpus nor the human gold set (1 500 rows, evaluation-only) ships in this repo. Not legal advice.
Attribution
Backbone: nreimers/mMiniLMv2-L12-H384-distilled-from-XLMR-Large (itself distilled from XLM-RoBERTa-Large).