GLiNER 2.5 is a NEW Provider package behind an enforced gliner2 major boundary¶
Status: Accepted (2026-08-25)
GLiNER 2.5 ships as one new package, packages/latence-gliner25, registering two entry
points — fused_entity_relation.gliner25 and redaction.gliner25 — pinned to
gliner2[local]>=2.0,<3. That window is disjoint from the gliner2[local]>=1.3,<2 the existing
latence-extract-gliner2 / latence-pii-gliner2 declare, and the disjointness is enforced at
dependency-resolution time in three places, never discovered at model load. Alongside it, a third
Chunker — chunk.page — cuts a document on its own page boundaries when it has a measured page map
and falls back to a 1024-token window when it does not; the wizard selects the whole set as ONE
engine answer so a mixed pair cannot be expressed.
Context¶
gliner2 2.0.0 was published to PyPI on 2026-08-24 (the previous line was 1.3.2, 2026-06-30).
It is a genuine major, and three facts read out of the 2.0.0 wheel decide everything below.
1. The checkpoint architecture changed. The GLiNER 2.5 checkpoints — gliner2.5-multi-v1
(287M, mDeBERTa-v3-base, multilingual), -base-v1, -small-v1, all fastino/, all Apache-2.0 —
are the new boundary architecture (gliner2.models.boundary.engine.BoundaryExtractor, a
sparse candidate decoder). Every 1.x checkpoint is span. AutoExtractor.from_pretrained
dispatches on the checkpoint's saved architecture and refuses to bridge them:
"Span and boundary heads are not checkpoint-compatible, and automatic architecture conversion is not supported." —
gliner2/auto.py
2. The loader changed. 2.5 is loaded through gliner2.auto.AutoExtractor. The legacy GLiNER2
class still exists and, by its own docstring, "remains span-only" — so it cannot load a 2.5
checkpoint at all.
3. The batched surface did NOT change. Both public model classes mix in
gliner2.inference.runtime.ExtractorRuntimeMixin, so batch_extract_entities /
batch_extract_relations keep the exact signature and the exact return envelopes the 1.x Providers
already drive:
# -> [{"entities": {label: [{"text", "confidence", "start", "end"}, ...]}}, ...]
# -> [{"relation_extraction": {rel_type: [{"head": {...}, "tail": {...}}, ...]}}, ...]
That third fact is the load-bearing one, and it is MEASURED, not read. Reading the wheel is
what suggested it — the two architectures build those shapes in different code, the span path
through ExtractorRuntimeMixin._extract_entities / ._extract_relations and the boundary path
through BoundaryExtractor._decode_entities / ._decode_relations, with the shared
format_results folding either into one envelope — but source reading cannot establish that two
implementations agree. So both were run. See the A/B
below: it is the evidence for this whole decision, not background. No Provider-side parsing change
was needed. Three real 2.0.0 deltas exist and none of them changes the contract: batch_size is the third positional parameter (it was in 1.3.2 too, and every
argument is passed by keyword here so it cannot silently become the threshold); a new
overlap_policy kwarg, not passed; and new *_long variants taking chunk_size/chunk_overlap,
deliberately unused — the Chunk Stage owns segmentation, and a second model-internal chunker would
produce offsets nothing downstream can ground (ADR-0021/0031).
The evidence: a cross-major A/B on the pod¶
Two venvs — the disjoint pin of this ADR made real — on RTX 2000 Ada 16 GB, driver 580.159.04,
2026-08-25. gliner2 1.3.2 + fastino/gliner2-multi-v1 (span head) and gliner2 2.0.0 +
fastino/gliner2.5-multi-v1 (boundary head), driven on the same text with the same labels
under identical transformers 4.57.6 / torch 2.4.1+cu124, diffing the structural signature of
the output — key sets, types, nesting; never values:
| field | verdict | |
|---|---|---|
entities_signature |
SAME | [{"entities": {<label>: [{confidence,end,start,text}]}}] |
relations_signature |
SAME | [{"relation_extraction": {<label>: [{head,tail}]}}] |
entity_row_keys |
SAME | ["confidence","end","start","text"] |
relation_row_keys |
SAME | ["head","tail"] |
relation_endpoint_keys |
SAME | ["confidence","end","start","text"] |
top_level_keys_entities |
SAME | ["entities"] |
top_level_keys_relations |
SAME | ["relation_extraction"] |
offsets_are_ints |
SAME | true |
n_entity_labels_returned |
SAME | 3 |
n_relation_types_returned |
SAME | 3 |
head_class |
DIFF | GLiNER2 → BoundaryExtractor |
gliner2_version |
DIFF | 1.3.2 → 2.0.0 |
Every structural field is identical. The only two that differ name which model is which.
That asymmetry is the whole decision. The inference contract carries across the major
unchanged — which is why one record contract, one set of Capability seams and one conformance case
shape serve both families, and why no parsing was written twice. The loader and the checkpoints
do not — AutoExtractor.from_pretrained raises ArchitectureMismatchError ("Span and boundary
heads are not checkpoint-compatible, and automatic architecture conversion is not supported"), and
the pins are disjoint. A version bump can only express the first half; it would silently redefine
what latence-extract-gliner2 is. A new package behind an enforced boundary expresses both: the
same contract, a different, non-interchangeable engine.
The key lists above are sorted sets — that is what the diff established. Key ORDER is deliberately not claimed anywhere, and no consumer in this framework reads these dicts positionally.
It is pinned so it cannot rot: latence_core.conformance.stub holds the contract as constants plus
a gliner2_shape_signature() that reproduces the pod's methodology, and all four fakes in this
workspace — the two stubs in core and the test-local fakes in latence-extract-gliner2,
latence-pii-gliner2 and latence-gliner25 — are asserted against that one definition rather than
against each other. test_gliner2_shape_contract.py additionally drives the span and boundary stubs
side by side and asserts their signatures are equal, which is the pod diff as a unit test, and
proves the checker rejects a renamed key, an added relation-level score and string offsets. A future
gliner2 release that moves any of these is one edit and several red tests, never a fake that quietly
drifts away from the API it stands in for.
4. There is no GLiNER 2.5 PII or guardrails checkpoint. As of 2026-08-25 the fastino PII models
(GLiNER2-Guardrails-PII-Multi, gliner2-privacy-filter-PII-multi) are still the 2.x-era span
checkpoints. "PII on 2.5" therefore means driving the general 2.5 checkpoint with PII labels
zero-shot.
5. The window is 4096 (model card), against the 768 the 1.x Providers default to.
Decision 1 — a new package, not a version bump of the existing one¶
latence-extract-gliner2 keeps gliner2>=1.3,<2 and keeps its span checkpoint; GLiNER 2.5 lands
beside it.
(a) Bump latence-extract-gliner2 to gliner2>=2,<3 — REJECTED¶
It reads like the smaller change and is the larger one. The library major, the loader class and the
model head all move together, so "a version bump" is a rewrite of the Provider's load path plus a
forced migration of every existing stack to a different checkpoint family — with no way to run the
old one. Worse, it is irreversible for an adopter: a pinned latence-extract-gliner2==0.2 would
silently mean a different model than ==0.1, which is exactly the "same name, different behaviour"
class this repository's per-Provider packaging (ADR-0016) exists to prevent. Nothing about 2.5 has
been measured yet (below), so a bump would also be a quality claim made in a version number.
(b) One package with a gliner2_major config knob — REJECTED¶
A single package cannot declare two disjoint pins. The knob would resolve to whichever gliner2 the
environment happened to install and fail at load on the other — i.e. it would move the failure from
resolution time to model-load time, which is the direction this repository refuses to move failures.
© A new package pinned to the new major — CHOSEN¶
The pin is the Provider's own (ADR-0016), the checkpoint family is the Provider's own, and an adopter installs the one they want. Both remain fully supported; neither is deprecated by this ADR.
Decision 2 — ONE package with two entry points, not two packages¶
GLiNER 2.5 is a single checkpoint family serving both Capabilities over one dependency pin, one
compatibility gate (latence_gliner25._compat) and one loader seam (_backbone.py). Two packages
would duplicate all three across distributions that must move in lockstep, and would make it
possible to install half a family — with the two halves free to drift onto different gliner2
minors, which is the drift the single pin exists to prevent.
The 1.x line being two packages is not a precedent to copy: they were built a release apart, against two genuinely different checkpoints (a general model and a dedicated guardrails+PII model). 2.5 has one checkpoint for both.
Decision 3 — the conflict boundary is ENFORCED, in three places¶
A stack naming fused_entity_relation.gliner25 beside redaction.gliner2 passes
latence stack validate: both Providers are real, both fulfil their Capability, and nothing in the
Stage contract is violated. It then cannot be installed. That is the worst possible place to learn
it, so the pair is refused three times before anything is fetched:
- The package pins. Disjoint windows; any resolver refuses the pair outright. Asserted from the
committed manifests in
packages/latence-gliner25/tests/test_gliner25_compat.py. deploy/env/*/environment.yaml.cpu-textandgpu-learned— the two environments that install a 1.x Provider — declaregliner2>=1.3,<2as an enforced conflict boundary, exactly asgpu-learnedalready declares thetransformers>=4.51.3,<5window (#198). Addingpackages/latence-gliner25to either failslatence env verifywithE-CONSTRAINT-CONFLICT— offline, import-free, lock-independent, on every commit throughscripts/verify-local.sh.packages/latence-core/tests/test_envspec.pyinjects exactly that mutation into a copy of each real environment and asserts the finding, plus the negative control that the boundary flags nothing else.latence_gliner25._compat. The gate for the environment the resolver never saw — a pod with a hand-pip installedgliner2, which is precisely what produced the #192 field failure on the 1.x redactor. An unsupported version, a missingAutoExtractor, aspancheckpoint, or a model without the batched entry points is a typedProviderError(ADR-0034) naming the supported range and the Provider that does load it — never a rawAttributeError.
The span-checkpoint refusal deserves its own sentence: AutoExtractor dispatches on the
checkpoint, so pointing model: at a 1.x checkpoint loads successfully and then behaves like a
different model under the same Provider name. A silent wrong-model run is worse than a crash, so it
is refused at load.
The consequence nobody wanted: the uv workspace¶
A uv workspace is one resolved environment — uv lock solves every member together — so the
three packages cannot all be members. uv lock says so directly:
Because latence-extract-gliner2 depends on gliner2[local]>=1.3,<2 and latence-gliner25 depends on gliner2[local]>=2.0,<3, we can conclude that latence-extract-gliner2 and latence-gliner25 are incompatible.
That refusal is correct, and it is the same fact as (2) at a different altitude. latence-gliner25
is therefore listed under [tool.uv.workspace] exclude. Moving its dependency behind an extra was
tried and does not help — uv locks members' extras too — and it would have hidden the pin from
the deploy/env boundary check, disabling (2).
Exclusion is cheap for the checks that work by directory (ruff, scripts/typecheck.sh, pytest, the
contract reference, the environment specs, the publish-readiness gates) and expensive for exactly
two things, both of which were found by running the gates rather than by reasoning, and both fixed:
- the release build.
uv build --package <name>resolves through the workspace and fails outright for an excluded package. The loops inrelease.yml,ci.ymland the runbooks now build by path (uv build "$pkg"), which is identical for members and non-members. - the SBOM.
uv export --all-packagesresolves the workspace, so an excluded package's dependency surface would be absent from a document that is stamped, checksummed and signed as the release's component inventory — the exact silent false negativerelease-integrity.shfails closed on.latence-gliner25therefore carries its own committeduv.lock, andscripts/generate-sbom.shfolds that lock's export into the requirements union (deduplicated by name+version) before the coverage check. It fails closed if an excluded package has no lock of its own, so the next exclusion cannot open the gap silently.
Decision 4 — the redaction claim is zero-shot, and says so¶
redaction.gliner25 drives the general 2.5 checkpoint with PII labels zero-shot, because no
2.5 PII checkpoint exists. That is a real capability — the GLiNER family is zero-shot span
extraction and PII types are ordinary label strings to it — and it is not the same thing as a
model trained on the task. Three consequences are built in rather than documented:
- No parity is claimed and no recall number exists.
docs/BENCHMARKS.mdrecords where the measurement will go and what produces it. TheProviderProfile.perf_notesays "ZERO-SHOT", "no parity", "UNMEASURED" in the descriptor itself, and a test asserts those words are there. guardrails: trueis REFUSED on the default checkpoint (a typedConfigErrornaming the deterministicscreening.content_keywordStage andredaction.gliner2as the two things that do work). A security control that is enabled and unbacked is worse than one that is off. It is accepted with a warning on an operator-suppliedmodel, because someone who fine-tuned their own 2.5 guardrails head knows something this package does not.- The ADR-0044 universal financial-PII floor is unaffected. It is a deterministic recogniser at
the single shared
finalize_redacted_chunkseam every Redaction Provider funnels through, sossn/credit_card/ibanare masked whatever the model does or fails to do. That floor is why a zero-shot PII Provider is shippable rather than merely plausible.
An inactive relation head gets the same treatment in the extractor: BoundaryExtractor's decoder
returns {} when enable_relations is false and format_results then fills every requested type
with an empty list — a well-formed, entirely empty answer with no error. The load seam logs that
loudly and the Provider reports its relation count per batch, so "zero relations" is visible rather
than inferred.
Decision 5 — chunk.page: page-preferred, 1024 otherwise¶
A third Chunker in latence_core.stages.chunk, behind the same Chunker seam and the same
offset-preserving engine as chunk.markdown / chunk.sentence_window, so the S3 round-trip
(chunk → offset → original page) and the v19 page_slice stay exact.
- A document with a MEASURED page map is cut on its page boundaries — one chunk per page. A page is an author-chosen unit of meaning, and a retrieval hit on one is citable as "page 7" with no further resolution; a window strategy cuts wherever the token budget runs out, which is a boundary nothing in the document agrees with.
- A document with none falls back to the windowed budget at 1024. Per ADR-0060 an absent page
map is a value, not a
page 1: anASSUMED_SINGLE_PAGEmap is the Parser saying "no page structure", so this Provider reads the map's origin, not merely its presence. Treating that single span as a page would emit one enormous chunk for a whole.txtfile while claiming it was a page. - A page over the budget still SPLITS. A page-sized chunk past the model window is a silent
truncation (tail entities lost, offsets unreliable, and for the Redactor, PII left unmasked);
a split is visible in the record count and every piece stays groundable.
min_tokensfolds a remnant back within a page, never across one.
The existing 512-token defaults of chunk.markdown / chunk.sentence_window are deliberately
untouched. They are baked into committed benchmark baselines and parity oracles; changing them
globally would silently invalidate every published number. 1024 is the default of the new
strategy, chosen to pair with the longer window: 1024 chunker tokens reach ~1229 mDeBERTa tokens at
the pod-measured 1.2× expansion, which model_window_for rounds to 1280.
Decision 6 — the wizard selects an ENGINE, not a Provider¶
SetupAnswers.engine is one answer; ExtractContribution and RedactContribution both read their
Provider off the same ExtractionEngine row, and ChunkContribution reads that row's chunker. There
is no combination of answers that emits a mixed pair — the invariant is structural, not advisory, and
it is asserted over the whole (engine × profile) product. The row also carries the engine's chunk
budget (so --engine gliner25 alone lands the page/1024 pairing, resolved on the struct so a
programmatic build_stack_config caller gets it too) and its documented checkpoint window, which is
a refusal: a budget whose derived max_len exceeds it is rejected at answer construction.
The 1.x engine's max_window is None — no ceiling is claimed, because none was ever verified for
that family. A first draft asserted one for symmetry and refused budgets that work today; the
negative control that keeps it honest is now a test.
Decision 7 — the advertised 4096 window is not a window of reliable extraction¶
This is the strongest finding in the integration and it changed the design. Measured on the pod
(RTX 2000 Ada 16 GB, driver 580.159.04, torch 2.4.1+cu124, transformers 4.57.6, gliner2 2.0.0,
fastino/gliner2.5-multi-v1, 2026-08-25). One call at max_len=4096 over a document seeded with a
repeated PII sentence, labels ["person name","email address","social security number"], threshold
0.4. coverage is the fraction of the document the returned spans reach:
| tokens | chars | pii_spans | max_char_seen | doc_chars | coverage |
|---|---|---|---|---|---|
| 674 | 2312 | 24 | 2310 | 2312 | 100% |
| 1262 | 4335 | 30 | 4293 | 4335 | 99% |
| 1850 | 6358 | 27 | 5738 | 6358 | 90% |
| 2438 | 8381 | 13 | 2869 | 8381 | 34% |
| 3026 | 10404 | 25 | 7493 | 10404 | 72% |
| 3950 | 13583 | 32 | 9227 | 13583 | 68% |
| 4874 | 16762 | 32 | 9227 | 16762 | 55% |
| 6050 | 20808 | 32 | 9227 | 20808 | 44% |
| 7562 | 26010 | 32 | 9227 | 26010 | 35% |
At 7,562 tokens the document holds 90 SSNs and the model returns 32 spans covering the first
35% of the text. It raises nothing, warns nothing, and returns a well-formed result;
max_char_seen pins at 9,227 however much longer the document gets. Two further modes on the same
hardware: 12,612 tokens with a single label returned zero spans, silently; and longer inputs
with three labels hit torch.OutOfMemoryError inside DeBERTa's disentangled_attention_bias
(2.14 GiB requested, 1.63 GiB free) — disentangled attention is quadratic, so one long chunk OOMs a
16 GB card outright.
Stated honestly: the filler is synthetic repetitive prose, so the percentages are directional, not a benchmark. What is not in doubt is the failure MODE — overflow is silent, partial and non-monotonic (2,438 tokens scored worse than 3,026) — and that useful recall ends far below 4096. Non-monotonicity is what makes this worse than a hard truncation: no threshold test catches it reliably.
1024 is an operator decision; the table confirms it¶
The reliable chunk budget is 1024 tokens — the windowed-fallback default of chunk.page and
the value the wizard emits for the gliner2.5 profile. It was decided, not read off the curve as
"still acceptable at 1,850": the curve's job is to show that 1024 sits comfortably inside the
99–100% region and that the region ends nowhere near the advertised ceiling.
The Providers' default max_len is derived from it — model_window_for(1024) — rather than
typed as a literal, so the Provider default and the wizard's emitted max_len cannot drift apart.
MAX_SUPPORTED_MAX_LEN = 4096 remains the library ceiling an operator may raise into; it is never
the default.
Two guards, because the failure is asymmetric¶
- The window bound. Above the safe window, extraction is warned (losing entities is a recall
trade an operator can see in the output and choose) and redaction is REFUSED with a typed
ConfigError. For a Redactor an over-wide window is not a trade: the Stage emits "redacted" chunks that still contain PII with nothing in the run reporting it, and the ADR-0044 floor only covers the deterministic financial set — not a name or an address. That is fail-open on a security control, and it is not an operator's to opt into by accident. It is accepted with a loud warning whenmodelnames an operator-supplied checkpoint, because the measurement describes one checkpoint and someone with their own evidence outranks it — the same shape as theguardrailsswitch. - The per-chunk guard. Bounding
max_lenbounds what the model is asked to read; it does not bound what it is handed. A chunk whose owntoken_countreaches past the window at the pod-measured 1.2× expansion is refused with a typedProviderErrornaming the budget knob. Refused rather than trimmed or re-chunked: a Capability that cannot be served is a loud error, and segmentation belongs to the Chunk Stage (ADR-0021), not to a Provider quietly re-doing it. The guard inverts exactly the derivation the default applies, so a wizard-emitted stack cannot trip it, and a chunk reportingtoken_count == 0is let through — the guard refuses on evidence, never on its absence.
chunk.page's split is now a measured requirement, not a safety argument: a dense page
routinely exceeds 2,000 tokens, which lands squarely in the collapse zone. A page over the budget
splits. No exceptions.
The offline fake models the truncation — it drops spans past a window and returns a well-formed
result — so the guard is what makes the fail-open test pass, not the fake's politeness. Verified by
removing the guard's evidence and watching the redaction test return masked_content set with the
email still in the clear.
Exploiting the longer window without shipping an OOM¶
gliner2 pads every text in a micro-batch to the batch's longest sequence, so one forward's padded
activation is microbatch_max_texts × max_len positions. The shared
resolve_microbatch_bounds derives its default from the detected card against the 768-window
reference point (4 × 768 = 3072 positions, measured at 21.6 GiB on the 32 GB pod). Taking that
unchanged under a 1280-token window would enlarge the padded forward by 1.67× — "raising max_len
without lowering max_texts" is an OOM shipped as a default. So the shared resolver still answers
(explicit knobs win, the VRAM derivation still lowers the envelope on a smaller card) and its answer
is re-expressed in positions and divided by the actual window. Nothing assumes the boundary
architecture is cheaper, because nobody has measured it — and the pod DID hit
torch.OutOfMemoryError inside DeBERTa's quadratic disentangled_attention_bias on a long single
chunk, which is the same envelope seen from the other side.
Measured on the real model (2026-08-25) — two defects a fake cannot catch¶
The Providers were written against the wheel and tested against a faithful fake. Both were then
run for real — gliner2==2.0.0 + fastino/gliner2.5-multi-v1, in an isolated venv on an Apple
Silicon macOS host (CPU) and on the CUDA pod — and the real load found two things no offline test
could have. Both are fixed here, and the second corrects a claim this repository already held.
1. The 2.5 checkpoint needs the tokenizer-config shim, and the old "only one checkpoint is
affected" claim is false. AutoExtractor.from_pretrained("fastino/gliner2.5-multi-v1") dies with
AttributeError: 'list' object has no attribute 'keys' at
transformers/tokenization_utils_base.py:1210 _set_model_specific_special_tokens, because the
checkpoint's tokenizer_config.json stores extra_special_tokens as a 10-element list — read
directly out of the downloaded snapshot, not inferred. That is the exact field failure ADR-0034
records for the 1.x PII checkpoint, and latence_pii_gliner2._tokenizer_compat already solved it —
while asserting that the sibling extract checkpoint's {} was "exactly why only this PII
checkpoint is affected". The 2.5 line reverts to the list shape, so the claim was a statement about
a file a third party controls, and it did not survive contact.
The fix is not a copy: coerce_list_extra_special_tokens is promoted into
latence_core.providers.perf, beside load_gliner2_model, and both packages call it — three
Providers, two packages, one shim. Its guarded / best-effort / idempotent posture is kept verbatim
(ADR-0034: no new silent error, only the removal of a crash), the stale sentence is corrected in
the same change, and the per-package module is deleted rather than left as a second copy. It is
applied once in load_gliner25_model, so both GLiNER 2.5 Providers and the uncompiled-reload path
get it without restating anything.
2. protobuf and sentencepiece are undeclared runtime dependencies. gliner2[local]==2.0.0
declares numpy / peft / safetensors / torch / transformers and no more; a clean install then fails
with transformers' PROTOBUF_IMPORT_ERROR. Same class of gap as the peft dep the 1.x packages
already record. sentencepiece is on the same path and was confirmed from the transformers 4.57.6
source rather than assumed — models/deberta_v2/tokenization_deberta_v2.py carries a top-level
import sentencepiece as sp and @requires(backends=("sentencepiece",)), and its
SENTENCEPIECE_IMPORT_ERROR sits beside the protobuf one in utils/import_utils.py; protobuf
simply raises first. Both are declared here, with their licences verified separately from the
installed distributions' own metadata on 2026-08-25 (ADR-0012): protobuf 7.36.0 declares
License: 3-Clause BSD License, sentencepiece 0.2.2 declares License-Expression: Apache-2.0 —
both permissive. They were invisible in the 1.x environments because something else happened to
pull them (deploy/env/cpu-text/requirements.lock records protobuf # via onnxruntime and
sentencepiece # via gliner), and neither of those distributions is in this package's closure.
Both are pinned protobuf>=7.36,<8 and sentencepiece>=0.2.2,<0.3 — the H-G1 convention every
other pin in this workspace follows, and neither is an exception. The floor is the version
actually verified, not a guess at the oldest release that might work; the cap is the boundary
past which the API may break, which matters more than usual here because both arrive through a
tokenizer path this package does not control, and protobuf has broken generated-code
compatibility across majors before. sentencepiece is a 0.x line, so its minor is the effective
major boundary — the same shape as gliner>=0.2.5,<0.3 and flashdeberta>=0.0.5,<0.1. (They
shipped uncapped in the first commit; that was an oversight, caught in review against the sibling
manifests.)
peft was re-checked and is deliberately not carried forward: 2.0.0's own METADATA declares
peft<1,>=0.10 under the local extra, so the explicit dep the 1.x packages carry (a pod-found
1.x gap) is redundant here.
What the real forward pass returns — the shapes the record contract rests on, confirmed rather
than assumed. NER: {"entities": {label: [{"text","confidence","start","end"}]}} with char offsets
present. Relations: {"relation_extraction": {rel: [{"head": {"text","start","end","confidence"},
"tail": {...}}]}} — the same shape latence-extract-gliner2 already unpacks, so the contract
carries over unchanged. Two details the fake now mirrors exactly: the head/tail key ORDER is
text, start, end, confidence (the 1.x docstring lists confidence second — key order only, no
contract impact), and head and tail carried the SAME score in every row observed, so nothing here
assumes they can differ without checking.
Zero-shot PII on the general 2.5 checkpoint found all four probes at high confidence (person
0.9992, email 0.9930, phone 0.9968, social security number 0.9875). That is a smoke signal, not
a benchmark: four probes on one hand-written string is not recall, and the measurement in
docs/running-the-benchmarks.md is still outstanding. No number from it appears anywhere as a
quality claim.
The fake was updated to be able to disagree about the first defect too: it refuses to load an
un-shimmed list-shaped config with the verbatim native AttributeError, so removing the shim call
turns a test red rather than passing silently. Verified by deleting the call and watching
test_the_shim_is_applied_before_the_backbone_load fail.
What was NOT built, and what remains unverified¶
- No throughput or quality number for GLiNER 2.5 exists. The checkpoint HAS now been loaded
and driven (above), which is what found the two defects — but a smoke run is not a measurement.
Every test in this change runs against a faithful offline fake of
gliner22.0.0 that reproduces the real degradations (bare strings withoutinclude_spans, an empty-but-well-formed relation result on an inactive head, aTypeErroron an unknown load kwarg, the un-shimmed tokenizer config). A fake proves the Provider parses, grounds and batches the real API; it says nothing about extraction or redaction quality.docs/running-the-benchmarks.mdnames what will produce the numbers and where they land. - No
deploy/env/environment resolvesgliner22.x yet. The compatibility matrix's cells for the boundary say so explicitly rather than leaving the absence to be inferred. - The weights licence is carried, not re-read. The library licence (Apache-2.0) was read out of
the 2.0.0 wheel's own
METADATAon 2026-08-25; the checkpoint's Apache-2.0 was taken from the Hugging Face model card and is cited with that date in theProviderProfile, per ADR-0012's weights-and-code-separately rule. The two newly-declared deps' licences WERE read from the installed distributions (above). - The pod hardware behind the real run is not named precisely here. The run was reported as an
isolated venv on an Apple Silicon macOS host (CPU) and on "the CUDA pod"; no SKU, driver or CUDA
version was recorded with it, so none is stated. The Blackwell/CUDA-13 runtime axis of the
compatibility matrix therefore stays
UNVERIFIED— a smoke run whose hardware nobody wrote down cannot promote a matrix cell. chunk.sentence_windowis not offered by the wizard's chunk strategies. It never was; adding a third strategy to the guided path is its own decision.
Consequences¶
- Two new Providers, auto-covered by the conformance suite (C1–C6) through per-Provider stub-scoped cases; conformance is a gate, not a quality guarantee.
- A third Chunker, auto-covered by the existing Chunk conformance case.
- One new answer (
engine) and one new escape hatch (chunk_strategy) inlatence setup; the default engine's emitted stack is unchanged. packages/latence-gliner25is outside the uv workspace and carries its own lock; the release builds by path and the SBOM unions the two locks. Both are gated.- A
gliner2>=1.3,<2boundary now bindscpu-textandgpu-learned, with matching compatibility-matrix cells (resolution measured offline, runtimeUNVERIFIED).