Provider base & profile¶
The optional AdapterBase convenience base and the frozen ProviderProfile descriptor —
the two halves of the Provider seam (ADR-0036).
See Authoring a Provider for the guide.
AdapterBase¶
adapter ¶
AdapterBase — the thin, optional base every provider adapter reuses (ADR-0036 §1).
A Provider is an adapter over a native library (CONTEXT Provider); the Capability
Protocol (ADR-0004) stays the stable I/O contract. Every adapter repeats the same
cross-cutting glue — pick a device, batch its inputs, turn a native library exception into
a typed :class:~latence_core.errors.LatenceError, and carry
:class:~latence_core.contracts.Provenance / :class:~latence_core.contracts.Classification
+ character offsets through the native call. P3-F2 factors that glue into one optional
:class:AdapterBase so a Wave-1 adapter is small and consistent, and those concerns are done
one right way instead of five subtly-different ways.
Non-negotiable framing (issue P3-F2):
- Optional, not a new seam. :class:
AdapterBaseis a convenience base. A Provider may still satisfy a Capability Protocol structurally without it — the shipping in-core reference Providers do, and are NOT force-migrated. No change to the Capability protocols; ADR-0036 rejected a second protocol layer. - Dep-light (ADR-0016). This lives in
latence-coreand pulls in no heavy deps (no torch, no model libs). It only orchestrates + maps; the adapter subclass owns its native dep. Device placement stays the adapter's job (it moves its own model, ADR-0003); :class:AdapterBaseonly resolves the :class:~latence_core.providers.device.DeviceDecisionand hands it over.
The four reusable pieces (compose them; the base does not force a god-class shape):
- :meth:
AdapterBase.device— resolveconfig.device(auto|cpu|cuda) against the subclass's declared :class:~latence_core.providers.profile.ProviderProfilevia P3-F1's :func:~latence_core.providers.device.select_device, exposing the chosen device (or the skip decision) to the subclass. Acompute="gpu"adapter on a CPU-only host sees the skip decision, never a fabricated device. - :meth:
AdapterBase.batched— group an input iterable intoconfig.batch_sizechunks and call the subclass's_run_batchwhen the profile is batch-capable, degrading to one-item calls otherwise. Deterministic input-order output regardless of batching, and no whole-corpus buffering (the streaming Runner's friend, ADR-0033). - :meth:
AdapterBase.guard— a context manager that catches an exception raised inside the native call and re-raises it as the right :class:~latence_core.errors.LatenceErrorsubtype (a genuine provider/runtime failure → :class:~latence_core.errors.ProviderError; an operator bad-config → :class:~latence_core.errors.ConfigError), tagged with the provider name + op and carrying no record content / PII. A framework :class:~latence_core.errors.LatenceErrorpasses through unwrapped. - :meth:
AdapterBase.carry_provenance+ :meth:AdapterBase.rebase_offsets+ :meth:AdapterBase.resolve_pages— map a native library's output back onto the input's Provenance/Classification and character offsets, reusing the existing :class:~latence_core.offsetmap.OffsetIndexseam and the document-scoped :class:~latence_core.page_index.PageIndexResolverseam (ADR-0031) — never a weaker hand-rolled offset mapper, and never a throwaway per-chunk page index — so the "offsets/page-provenance intact" invariant is the default.
ProvenanceCarrier ¶
Bases: Protocol
The structural source :meth:AdapterBase.carry_provenance reads: anything with a frozen
:class:~latence_core.contracts.Provenance + :class:~latence_core.contracts.Classification.
P3-F2's helper originally typed its source as a :class:~latence_core.contracts.Record, but
the FIRST real adapter over it (H-A1, the pdfplumber Parser) exposed a genuine misfit: a
Parser's source is a :class:~latence_core.contracts.ParserInput (a raw document, ADR-0019),
which carries Provenance + Classification but is not a Record subclass — so a real
Parser could not reuse the helper for a whole-document carry without a hand-rolled cast (the
exact "weaker mapper" the audit forbids). Widening the source to this structural Protocol is
the minimal, backward-compatible fix: every Record satisfies it (it has both attributes),
and so does a ParserInput, so both the sub-document carriers (mentions/chunks, Records)
and the whole-document Parser carry through the same base helper. The helper only ever reads
.provenance (frozen, model_copy-ed) and .classification (returned unchanged), so
the Protocol names exactly those and nothing more.
AdapterBase ¶
Optional base carrying the cross-cutting glue every provider adapter repeats (ADR-0036 §1).
A subclass declares its :class:~latence_core.providers.profile.ProviderProfile (a
profile classmethod or a PROFILE ClassVar, the P3-F1 forms) and receives the
Stage config dict at construction. It then composes the four helpers:
- :meth:
devicefor the resolved :class:~latence_core.providers.device.DeviceDecision, - :meth:
batchedfor deterministic, streaming-friendly batching, - :meth:
guardfor native-exception → typed-taxonomy mapping, - :meth:
carry_provenance/ :meth:rebase_offsetsfor Provenance/offset carry.
It is not a Capability — it names no parse/extract/… method and imposes no
protocol. A subclass still implements whichever Capability method it fulfils, so
isinstance(provider, Parser) (etc.) is satisfied structurally exactly as for a
Provider that does not use this base. The base is convenience, never a required layer.
Source code in packages/latence-core/src/latence_core/providers/adapter.py
config
property
¶
The Stage config dict the Provider was constructed with (a defensive copy).
provider_name
property
¶
A stable, human-readable provider name for error tags (never carries content).
Defaults to the subclass's class name — used only to label a mapped
:class:~latence_core.errors.LatenceError (provider + op), never to carry any record
content or PII. A subclass may override to surface its registered entry-point name.
device
property
¶
The resolved :class:~latence_core.providers.device.DeviceDecision for this adapter.
The subclass reads self.device.device ("cpu"/"cuda"/None) to place its
model, and self.device.skipped to know it was skipped-with-flag on a host that
cannot run it. Device placement stays the adapter's job (ADR-0003) — this only
resolves the decision.
declared_profile
classmethod
¶
This adapter's declared :class:ProviderProfile, read via :func:profile_of.
Reads whichever declaration form the subclass uses (a profile classmethod or a
PROFILE ClassVar), reusing the P3-F1 reader — never fabricating one. A subclass
that declares none returns None (the device seam then follows the request, treating
it as compute="either"). A malformed declaration surfaces as TypeError from
:func:profile_of — the same fail-loud contract the Runner now honors (issue P3-F2 §2).
Source code in packages/latence-core/src/latence_core/providers/adapter.py
load_perf_kwargs ¶
The load-time perf kwargs (dtype + attn) to spread into from_pretrained (W8).
Resolves the load dtype (bf16 on GPU / fp32 on CPU by default, or config["dtype"]
override) and the attention implementation (sdpa default, flash_attention_2 only
when the flash_attn package is importable and the config permits it) against this
adapter's resolved device (:attr:device), returning the two kwargs HF from_pretrained
reads::
model = Model.from_pretrained(model_id, **self.load_perf_kwargs())
Applied once per load so a learned Provider never loads bare fp32 (the ~2× miss). An invalid
dtype config is an operator :class:~latence_core.errors.ConfigError naming the provider
never a silent wrong dtype. The FA2 load can still raise on a model that does not support
it — the adapter wraps the load so that surfaces as a graceful sdpa retry, never a crash.
Source code in packages/latence-core/src/latence_core/providers/adapter.py
maybe_compile ¶
torch.compile the model when config["compile"] is truthy — guarded (W8).
Off by default (returns the model unchanged); on, wraps the forward with
torch.compile(mode="reduce-overhead") behind a broad guard so any compile/warmup failure
logs and returns the uncompiled model — a bad compile costs only throughput, never
correctness or a crash. The compiled module is a drop-in for the eager one (same outputs).
Source code in packages/latence-core/src/latence_core/providers/adapter.py
batch_size ¶
The validated batch size (config.batch_size, default :data:DEFAULT_BATCH_SIZE).
Must be >= 1 — a non-positive or non-int batch_size is an operator
:class:~latence_core.errors.ConfigError (a batch of zero would never make progress).
Source code in packages/latence-core/src/latence_core/providers/adapter.py
batched ¶
batched(
items: Iterable[_In],
run_batch: Callable[[list[_In]], Iterable[_Out]],
) -> Iterator[_Out]
Group items and call run_batch per group, yielding outputs in input order.
The batching helper the streaming Runner (ADR-0033) is a friend of:
- When the declared profile is batch-capable (
profile.batch is True),itemsare grouped into :meth:batch_sizechunks andrun_batchis called once per chunk. - When it is not batch-capable (or no profile is declared), it degrades to
one-item batches —
run_batchis called with a single-element list at a time — so a non-batch adapter reuses the exact same code path.
Ordering is deterministic: outputs are yielded in the order run_batch returns
them, which for a correct adapter mirrors input order (a batch preserves its members'
order; the batches themselves are consumed in input order). The input iterable is
consumed lazily one batch at a time — the whole corpus is never buffered, so a
streamed input stays streamed (memory bound O(batch_size), not O(corpus)).
run_batch maps a list of inputs to an iterable of outputs; a subclass wraps its
native call there (typically inside :meth:guard). This helper adds no error mapping of
its own — the subclass owns that so the op label is precise.
Source code in packages/latence-core/src/latence_core/providers/adapter.py
guard ¶
Map an exception raised inside the with block to the typed taxonomy (ADR-0034).
Wrap the native library call so a native exception becomes the right
:class:~latence_core.errors.LatenceError, tagged with the provider name + op and
carrying no record content / PII (only the provider, the op, and the native
exception's type name — never its message, which could echo an input):
- a native exception whose type is in :attr:
config_exceptions→ a :class:~latence_core.errors.ConfigError(an operator-supplied bad config: a bad model_id, a bad parameter). Fix the config, not the code. - any other native exception → a :class:
~latence_core.errors.ProviderError(a genuine provider/runtime failure).retryable=False— the adapter does not presume a native fault is transient; an endpoint adapter that knows better raises its own retryable ProviderError directly. - a framework :class:
~latence_core.errors.LatenceErrorraised by the framework (e.g. a contract check the adapter itself made) passes through unwrapped — it is already typed and already carries the right category; re-wrapping would relabel it.
The op is a short, static verb the adapter names ("parse", "extract",
"embed") — not derived from input — so the message is a stable, greppable label.
Crucially the caught exception's message is deliberately not interpolated into the
raised message (only type(exc).__name__), matching the Quality Report's no-PII
discipline: a native lib that echoes the offending input text in its error must not leak
it into the framework error a report or log records.
Source code in packages/latence-core/src/latence_core/providers/adapter.py
carry_provenance
staticmethod
¶
carry_provenance(
source: ProvenanceCarrier,
*,
char_start: int | None = None,
char_end: int | None = None,
page_start: int | None = None,
page_end: int | None = None
) -> tuple[Provenance, Classification]
Carry a source's Provenance + Classification onto a produced record.
source is any :class:ProvenanceCarrier — a :class:~latence_core.contracts.Record
(a sub-document mention/chunk) OR a :class:~latence_core.contracts.ParserInput (a Parser's
raw-document source, ADR-0019); both carry the two fields this helper reads, so a real
Parser reuses it for a whole-document carry rather than hand-rolling the chain (H-A1).
The default so an adapter never hand-rolls the Provenance chain: it takes the source's
immutable :class:~latence_core.contracts.Provenance and narrows only the
offset/page fields the produced record occupies (a sub-record's own span), leaving the
source lineage (source_uri/document_id/file_*) intact, and returns the
source :class:~latence_core.contracts.Classification unchanged (Classification is
inherited by every downstream record, CONTEXT Classification).
Only the offset/page fields that are passed are overridden; the rest of the Provenance
is copied verbatim via model_copy(update=...) (Provenance is frozen). Passing none
returns the source Provenance/Classification exactly (a pure carry).
Source code in packages/latence-core/src/latence_core/providers/adapter.py
rebase_offsets
staticmethod
¶
rebase_offsets(
local_start: int,
local_end: int,
*,
chunk_char_start: int,
offset_map: OffsetMap | None
) -> tuple[int, int]
Rebase a chunk-local half-open span to true original-markdown offsets (ADR-0031).
Reuses the existing :class:~latence_core.offsetmap.OffsetIndex seam — not a weaker
hand-rolled mapper (issue P3-F2). Chunk content is markup-stripped, so the naive
chunk_char_start + local shift is only a lower bound once markup was stripped before
the span within its chunk; the carried :class:~latence_core.contracts.OffsetMap corrects
it. When a chunk carries no offset map (a pre-v11 chunk, or a markup-free chunk), this
degrades to the exact chunk_char_start + local shift — the identity path, unchanged.
Returns the half-open [doc_char_start, doc_char_end) span in the parent document's
assembled markdown, ready for :meth:resolve_pages and :meth:carry_provenance.
Source code in packages/latence-core/src/latence_core/providers/adapter.py
resolve_pages
staticmethod
¶
resolve_pages(
chunk: ChunkRecord,
doc_char_start: int,
doc_char_end: int,
*,
resolver: PageIndexResolver
) -> tuple[int, int]
Resolve a document-offset span to its source page(s) via the page seam (ADR-0031).
Goes through :class:~latence_core.page_index.PageIndexResolver — the ONE seam that
owns offset→page resolution — so a produced sub-record cites its own page, not
merely the parent chunk's inherited range. The resolver owns the half-open end clamp
and the corpus-wide drift roll-up.
doc_char_start/doc_char_end must be offsets inside chunk, which is what
:meth:resolve_offsets produces. Since v19 the chunk carries its own page spans, so
resolution needs nothing but the record in hand: any order, any subset of the stream,
no observe and no per-document state::
resolver = PageIndexResolver() # one per extract() call, for the roll-up
for chunk in chunks:
...
pages = self.resolve_pages(chunk, doc_start, doc_end, resolver=resolver)
A chunk whose producing Chunker stamped no page spans raises
:class:~latence_core.errors.PageSliceMissingError. It does NOT degrade to the chunk's
inherited range: that produced a page that looks resolved and is not, which is the
exact provenance regression (audit R2/R4) this seam exists to prevent.
For a span whose endpoints may lie in DIFFERENT chunks — a relation's covering span —
use :func:~latence_core.page_index.pages_for_covering_span instead; no single chunk
can answer for it.
Source code in packages/latence-core/src/latence_core/providers/adapter.py
validate_batch_size ¶
Validate a config.batch_size value to a positive int, or raise ConfigError (W7).
The shared, dep-light validator the learned ENCODER providers reuse so a batch_size knob
is checked one right way whether or not the provider subclasses :class:AdapterBase
(:meth:AdapterBase.batch_size delegates here). A batch_size must be an int >= 1 — a
batch of zero would never make progress, and a bool/float/str is a config typo, not a
valid size — so a non-positive or non-int value is an operator
:class:~latence_core.errors.ConfigError naming the provider, never a silent fallback.
Source code in packages/latence-core/src/latence_core/providers/adapter.py
ProviderProfile¶
profile ¶
ProviderProfile — a provider's declared, typed descriptor (ADR-0036 §2).
Every Provider (CONTEXT Provider) declares a small, frozen ProviderProfile:
what compute it needs, its approximate memory footprint, its model id + verified
SPDX license, whether it is deterministic and batch-capable, and (for endpoint/API
providers) its per-1k cost. The registry surfaces it (ProviderRegistry.profile),
and it drives the Phase-3 harnesses: skipping GPU providers on a CPU-only host
(:mod:~latence_core.providers.device, the G1 posture), device routing, and the
license/cost/compute columns of the bake-off (P3-F5) — all from one declared
object, so adapters and harnesses never churn.
This module is deliberately dependency-light — a Pydantic v2 contract in the same
style as :mod:latence_core.contracts, with no torch/heavy deps (ADR-0016) — so
pip install latence-core stays lean and the registry can read a provider's
profile off the class object it already loads.
A provider declares its profile via a profile classmethod (preferred — a provider
can compute it from config) OR a PROFILE ClassVar; :class:HasProfile is the
runtime_checkable protocol the registry uses to detect either structurally, and
:func:profile_of reads whichever a provider class declares.
ProviderProfile ¶
Bases: BaseModel
A provider's declared capabilities + enterprise-readiness columns (ADR-0036 §2).
Frozen and strict (extra="forbid") — the descriptor is a stable, immutable
fact about a provider, read by the registry, the device seam, and the bake-off.
Fabricating one is forbidden: a provider that declares none surfaces as None
from the registry, never a synthesised default (P3-F1 scope §3).
Fields (ADR-0036 §2):
compute—"cpu"|"gpu"|"either": what the adapter needs.memory_mb— approximate peak resident footprint in MiB, declared by the provider (not measured here).>= 0.model_id— the HF/model identifier, orNonefor a pure-code provider (gazetteer, pattern) that ships no weights.license— the headline/effective SPDX identifier (e.g."Apache-2.0","MIT") or the sentinel :data:UNVERIFIED_LICENSEwhen it could not be verified in-slice. Must be consistent withlicense_code/license_weights.license_verified—Trueonly when BOTH weights and code were checked per ADR-0012, and the verification evidence is present (H-B1: a bareTruewith no evidence is un-constructable). The :data:UNVERIFIED_LICENSEinvariant forbids a verified unknown.license_code— SPDX of the code/library (a provider's own package code is the repo's"Apache-2.0"). ADR-0012 records code separately from weights.license_weights— SPDX of the weights, orNonefor a pure-code provider that ships no model (model_id is None). ADR-0012 records these separately.license_source— a citation for the verification: the HF model-card URL, the GitHub LICENSE URL, or the repo's own LICENSE path — where a human actually checked. Required wheneverlicense_verifiedisTrue.license_verified_on— ISO date (YYYY-MM-DD) the check was performed. Required wheneverlicense_verifiedisTrue.deterministic— whether identical input + config yields identical output (the Baseline/G1 determinism signal a bake-off reports).batch— whether the adapter supports batched calls.cost_per_1k— for an endpoint/API provider, the cost per 1k units;Nonefor a local/in-process provider (no per-call cost).>= 0when present.
HasProfile ¶
Bases: Protocol
Structural protocol: a provider class that declares a :class:ProviderProfile.
A provider surfaces its profile via a profile classmethod (preferred, so a
provider can compute it from config) OR a PROFILE ClassVar. This protocol names
the profile classmethod shape so the registry can detect a declaration
runtime_checkable-y — without importing the provider's heavy deps beyond the
class object it already loaded. :func:profile_of is the reader that accepts either
the classmethod or the ClassVar form.
profile
classmethod
¶
profile_of ¶
Read a provider class's declared profile, or None if it declares none.
Detects either declaration form on the class object the registry already loaded, never fabricating a profile for a provider that declares none (P3-F1 §3):
- a
profileclassmethod (preferred) — called with no args; or - a
PROFILEClassVar holding a :class:ProviderProfile.
A provider with neither returns None. A provider whose profile attribute is
present but is not a callable/classmethod returning a :class:ProviderProfile (or a
PROFILE that is not one) raises TypeError — a malformed declaration is a
hard error, not a silently-dropped profile, so a typo surfaces rather than reading
as "no profile declared".
A subtlety the reader MUST respect (else it corrupts a legitimate Provider): the
:class:~latence_core.capability.Profiler Capability method is itself named
profile — an instance method profile(self, documents, mentions), not a profile
declaration. Calling that with no args would raise a spurious TypeError and mislabel a
perfectly valid Provider as "malformed". So the profile-declaration form is recognised only
as a genuine @classmethod (accessed on the class, it is bound with __self__ is
provider_cls); a plain instance method named profile is NOT a declaration and falls
through to the PROFILE ClassVar / None path. The malformed-declaration contract
still holds for a real profile classmethod that returns a non-:class:ProviderProfile.
Source code in packages/latence-core/src/latence_core/providers/profile.py
Device selection¶
device ¶
Dep-light device routing seam (ADR-0036 §1/§2, ADR-0016).
Device placement stays the adapter's job (it moves its own model to the resolved device, ADR-0003/0036 §2); this seam does NOT move models. It answers two questions, dependency-light and without importing torch (torch is not a core dep, ADR-0016):
- :func:
cuda_available— is a CUDA GPU present? Deterministic under an explicitLATENCE_CUDA=0|1env override (for CPU-only CI and seeded tests); otherwise a best-effort probe that defaults toFalsewhen unknown. - :func:
select_device— given a requested device (auto|cpu|cuda) and a provider's :class:~latence_core.providers.profile.ProviderProfile, return a :class:DeviceDecision: the effective device ("cpu"/"cuda"), or a skip-with-flag decision (skipped=True+ a reason) when a GPU-only provider is requested on a CPU-only host. It never raises for that case — the G1 posture is flag-and-skip, so the stack harness records the skip in its report rather than crashing (no fabricated GPU numbers).
DeviceDecision
dataclass
¶
The typed outcome of :func:select_device — a routing result, not a bare string.
So a harness can record a skip in its report (never a fabricated GPU number):
device— the effective device ("cpu"or"cuda") to place the model on, orNonewhenskipped(there is no device to place on).skipped—Truewhen a GPU-only provider was requested on a CPU-only host: the provider is skipped-with-flag (the G1 posture), not run and not crashed.reason— a human-readable explanation of the routing (why this device, or why the skip), for the bake-off / stack-validation report.
cuda_available ¶
Detect a CUDA GPU without importing torch (ADR-0016), override-driven for CI.
Probe order (deterministic under the override):
- Honor an explicit
LATENCE_CUDA=0|1env override first — the CI/test knob that makes a CPU-only host reproducible and lets a test drive the cuda path. - Otherwise, a best-effort torch-free probe: the presence of the
nvidia-smitool onPATHOR an/dev/nvidia*device file. Either is a strong signal a CUDA device exists; neither imports torch or a CUDA library. - Default to
Falsewhen unknown — a CPU-only host is the safe assumption, so a GPU provider is skipped-with-flag rather than attempted and crashed.
Under the override this is fully deterministic (no filesystem/PATH dependence), which is what the seeded conformance/stack tests rely on.
Source code in packages/latence-core/src/latence_core/providers/device.py
select_device ¶
Route a requested device against a provider's profile (skip-with-flag, never raise).
requested is the device config value (auto|cpu|cuda); profile is the
provider's declared :class:~latence_core.providers.profile.ProviderProfile (or
None when it declares none). The G1 posture is skip-with-flag, never raise for
the GPU-on-CPU-only case, so the stack harness records the skip.
Routing (per ADR-0036 §2):
profile.compute == "cpu"→ always"cpu". Evenrequested="cuda"resolves to CPU (with a reason noting the override) — a CPU-only provider has no GPU path, so this is not a skip, just a clamp.profile.compute == "gpu"→ needs a GPU. If CUDA is available, resolve to the requested device (cpuis still honored if explicitly asked, elsecuda); if NOT available, skip-with-flag —requested="auto"or"cuda"both skip,requested="cpu"clamps to CPU only if the provider could run there, which acompute=="gpu"provider cannot, so it also skips.profile.compute == "either"(orprofile is None) → follow the request:cpu→ CPU;cuda→ CUDA if available else skip-with-flag;auto→ CUDA when available, else CPU.
Never raises for the skip case; a genuinely malformed requested string is still a
ValueError (config error), surfaced before any routing.
Source code in packages/latence-core/src/latence_core/providers/device.py
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | |