CapabilityProvider
The plugin contract every extension distribution implements.
Protocols:
CapabilityProvider.descriptors() -> tuple[CapabilityDescriptor, ...]CapabilityProvider.create(capability_id: str) -> Any
CapabilityProvider is the plugin-facing boundary for advertising processing
capabilities and creating their runtime implementations. Installed
distributions expose providers through the indx.capabilities Python
entry-point group, allowing indx to discover extensions without importing them
directly.
descriptors() returns lightweight public metadata for the provider’s
capabilities. create() resolves one of those capability IDs to the object that
performs the actual work.
Discovery and planning need to know what work is available, but they should not load OCR engines, model weights, GPU runtimes, remote clients, or other expensive dependencies. Separating description from creation lets the registry build a complete inventory cheaply while deferring heavyweight initialization until execution selects a capability.
Stable capability IDs also give snapshots and plans a provider-neutral key. Routing can refer to a capability without depending on its package or concrete implementation class.
Contract
Section titled “Contract”descriptors() returns an immutable sequence of
CapabilityDescriptor
values. Each descriptor declares:
- A stable ID and version.
- Its capability kind.
- Supported devices and media types.
- Runtime requirements.
- Availability and, when unavailable, an explicit reason.
create(capability_id) accepts one advertised capability ID and returns its
runtime implementation: a
PageReader,
which reads the named pages out of one loaded source and returns a PageOutput
for each.
A capability that can specialize also implements
SignatureDetector,
whose detect() recognizes what kind of document it is holding. That runs at
plan time, so it inherits preflight’s budget: cheap, local, deterministic, and
free of OCR, models, rendering, and network. A detector that cannot answer within
that budget answers with nothing. It receives bytes rather than extracted text,
so the planner never has to grow a text extractor and a detector stays free to
decide how much of the document it wants to look at.
A signature reaches the caller as a SignatureMatch on the plan, and the
capability it nominates is placed ahead of the generic routing ladder rather
than in place of it. A signature is a guess; keeping the ladder behind the
specialist is what makes a wrong guess cost one cheap attempt instead of the
document.
A signal is a property of the document type, never of one document (ADR-0045): structure the format states, or vocabulary the type carries by definition (an invoice says “invoice”), and it is shown firing on two independent documents before it ships. A word one author put on one document is not a signal, and a name a deployment would want to vary is data on the output, not evidence.
Kind describes the work, never the door. A capability that generically reads
its media type declares native_extraction, however specialized the machinery
behind it; parser means the capability is only correct about a document
something recognized first, and it reaches a route only through nomination. The
worked example pairs the two on purpose: acme-plaintext reads the format,
acme-purchase-order specializes when its own signature matches. A media type
only a parser declares, planned without signature detection, comes back as an
unsatisfied plan naming the parser and the flag.
One protocol covers every reading kind. Native extraction, OCR, a vision model and a human queue differ enormously in what they cost and how they fail, and not at all in what they are asked. A protocol per kind would put the routing ladder’s rungs into the type system, and the policy routes over kinds precisely so a third-party OCR distribution behaves exactly like the first-party one.
The set of kinds is closed, and so is the ladder over them. CapabilityKind has
six members and an install cannot add a seventh, because a kind is a policy
position – a price, a quality, a place in the fallback order – and a
distribution that could declare one would be deciding what indx routes to rather
than supplying something to route to. A distribution whose work is none of the
six, such as classification, redaction, translation, speech or table structure,
declares the nearest kind, inherits its ladder position, and asks the deployment
to correct the numbers through INDX_ROUTING_ECONOMICS. Adding a kind, or making
a preflight signal an observer already emits mean something, is a first-party
policy change with a POLICY_VERSION bump.
The numbers are core-owned for the same reason, and the cost of that is worth
stating rather than leaving to be discovered. A descriptor carries no cost,
quality or latency, and the only override is an environment variable an operator
sets. So a hosted OCR charging $0.05 per page inherits the per-kind default of
$0.0005, is admitted against a caller’s maximum_cost_usd on a number a hundred
times too low, and the plan’s estimates.cost_usd publishes that figure as its
price until the deployment writes the real one down. A self-declared price would
be what a capability uses to buy its way into a route; the party paying the bill
is the one with a reason to be honest about it.
Responsibilities and guarantees
Section titled “Responsibilities and guarantees”- Keep descriptor discovery lightweight and free of model initialization.
- Advertise stable, non-empty IDs and versions.
- Report availability consistently: available capabilities have no unavailable reason, while unavailable capabilities provide one.
- Create the implementation associated with the requested advertised ID.
- Keep credentials, private endpoints, and implementation-only configuration out of descriptors.
- State each line it located on
PageOutput.lineswhen its engine knows positions, asgeneric-ocrdoes from the boxes the recognizer reports:TextLine(text, bbox), the box normalized likeBlock.bbox. The lines never reach the wire;indx-chunker-linescuts them into chunks with rectangles, and a reader that states none leaves the page to the chunker floor (ADR-0047). - Return one
PageOutputfor every page aread()call was given. There are two ways to say a page could not be read, and the executor answers both by descending to the plan’s next capability: produce nothing for it, or produce one carryingstatusfailedand areason. Prefer the second – the reason reaches the trace, where a caller can see it. An emptytextsays the opposite of both: the page was read and held nothing. statusunreadableis not a failure to read but a verdict about the content, and it is kept rather than failed over. That is what makes the terminal manual-review rung terminal; a capability that means “try someone else” saysfailed.- Report a per-page confidence on
CONFIDENCE_METADATA_KEYwhen the engine produces one, the way a billed capability reportsCOST_METADATA_KEY. It reachesExecutionActuals.qualityas a self-report, and a deployment that has setINDX_VALIDATION_MIN_CONFIDENCErefuses output below it. A capability with no calibrated number reports none: absent is no opinion, never zero. - Avoid coupling the provider package to router, executor, or application implementations.
A provider that embeds also satisfies EmbeddingSpaceProvider, whose
embedding_spaces() declares whole spaces: dimension, metric, normalization,
and every embedder in them. Each embedder is at the same time one of the
provider’s EMBEDDER-kind descriptors, which is how availability and
create() reach it; for those IDs create() returns a
VectorEncoder,
one encode() for text and image alike, fed the modality the embedder
declared. A space is declared whether or not its embedders can run today, and
one provider declares a whole space: two implementations share a space ID only
inside one distribution, where a test can demonstrate their vectors compare.
A provider that makes a new media type plannable also satisfies
SourceObserverProvider, whose source_observers()
declares the observers it ships. It is optional the same way: a provider reading
a format indx already observes never implements it. Unlike a space, an observer
names no ID and joins no descriptor, so it does not move the snapshot –
descriptors() returning empty is a truthful declaration for a distribution
that observes and reads nothing, which is exactly what indx-observer-pdf,
indx-observer-image, indx-observer-office, indx-observer-text and
indx-observer-email are. Their readers ship
separately — indx-capability-native-extraction and
indx-capability-office-extraction — so what makes a format plannable and what
makes it routable are two installs, and an operator can have either without the
other.
Place in the system
Section titled “Place in the system”The capability registry discovers providers and combines their descriptors into a deterministic snapshot. The planner selects descriptor IDs from that snapshot. During execution, the selected provider creates the corresponding runtime implementation lazily.