You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[API Proposal]: IDocumentExtractionClient — a document-extraction capability as its own peer library (Microsoft.Extensions.DocumentExtraction)
Important
Review status: This proposal describes the current 32-type [Experimental] surface implemented
by #7588. The prototype, provider surveys, and implementation validate that the shape is feasible;
they do not replace reviewer approval.
Confirmation requested in this review: the standalone Microsoft.Extensions.DocumentExtraction(.Abstractions) home and name; the neutral Document*
model living there for v1; the GetService / RawRepresentation / AdditionalProperties posture;
the v1 middleware boundary; and the next formal approval gate.
Implementation status (2026-08-10):#7588 is open and non-draft at a215825ae2; its Ubuntu,
Windows, coverage, aggregate CI, and CLA checks are green.
Proposal change history and superseded intermediate states
Updates
2026-07-30 — Moved out of Microsoft.Extensions.AI into its own peer library, Microsoft.Extensions.DocumentExtraction. Per review steer, document extraction is its own domain (like VectorData / DataIngestion), not an IChatClient sibling inside M.E.AI: it pulls structured content out of documents, complementing DataIngestion, which feeds content into RAG. The capability keeps the M.E.AI building-block shape (abstraction + delegating base + builder + logging/OpenTelemetry/configure-options middleware + DI) and references Microsoft.Extensions.AI.Abstractions internally (e.g. DataContent) without carrying the "AI" brand. This absorbs the shared-model + family-rename tracks below (both now resolved by the move) and lets a provider team (e.g. Azure AI Document Intelligence) own an IDocumentExtractionClient impl without an "AI" branding dependency. Naming flips from the M.E.AI-internal verb scheme (SpeechToText) to the peers' domain-noun convention: a neutral Document* content model + DocumentExtraction* operation/client types. New diagnostic id MEDE0001 (peer precedent: VectorData MEVD9001). The extraction builds green as a straw-man (both new packages + M.E.AI(.Abstractions) with OCR removed + the DataIngestion consumer + both new test projects, 60 tests). The speclet below reflects the new Document* content model and DocumentExtraction* operation/client names, with public-API baselines regenerated across all five TFMs (netstandard2.0, net462, net8/9/10). Methods (ExtractAsync/ExtractPagesAsync/GetService/AsBuilder) are unchanged. The surface stays one [Experimental] unit so a review-driven name change remains a single mechanical re-rename.
2026-07-23 — Pre-review spikes: per-page coordinate model, selective promotes, geometry primitives and GetService kept. SPIKE-07/08 moved DocumentCoordinateUnit / DocumentCoordinateOrigin back onto DocumentPage (reported per page, not per document — engines emit different units for different pages in mixed image/PDF batches, per Google Page.Dimension and Azure DI DocumentPage.Unit), and grouped DocumentPage.Width / Height into a new DocumentPageDimensions value type. SPIKE-06's 14-engine raw-output inventory kept RawRepresentation / AdditionalProperties and promoted per-cell BoundingRegion / Confidence / RawRepresentation / AdditionalProperties onto DocumentTableCell plus RawRepresentation onto DocumentPage. SPIKE-09 kept the three OCR-owned geometry primitives (no cross-platform BCL type carries the page-scoped, rotation-capable DocumentBoundingRegion polygon). SPIKE-04/05 kept IDocumentExtractionClient.GetService (one optional seam for provider metadata, provider-SDK escape, and adapter unwrap). Surface is now 32 public types (adds DocumentPageDimensions).
2026-07-22 — Superseded intermediate state: API-review reshape with document-level coordinates. Collapsed the parallel DocumentPage.Blocks/Tables/Images into one reading-order DocumentPage.Elements over a new polymorphic DocumentElement base (DocumentBlock/DocumentTable/DocumentImage derive; project with OfType<T>()), and added optional nested DocumentTableCell.Elements. The coordinate metadata was temporarily moved to the document result; the July 23 provider evidence superseded that placement and restored it per page. Renamed ExtractStreamingAsync → ExtractPagesAsync and OcrResponseUpdate → DocumentExtractionPageResult (non-null Page, dropped Status); Markdown → Text; added DocumentExtractionUsage token counts; added DocumentTableCellKind.RowHeader/RowSection; dropped response ModelId, DocumentPage.Confidence, and DocumentExtractionOptions.IncludeImages; DocumentBoundingRegion.FromRectangle now takes float. Surface was 31 public types at this point.
2026-07-15 — API-review alignment (family symmetry). Replaced the unary-only shape with the family's unary + streaming pair: added IAsyncEnumerable<OcrResponseUpdate> ExtractStreamingAsync(...) (the IChatClient.GetStreamingResponseAsync twin), OcrResponseUpdate, and an OcrResponseUpdateExtensions.ToDocumentExtractionResult/ToDocumentExtractionResultAsync reducer, and removed IProgress<DocumentExtractionProgress> and DocumentExtractionProgress (progress now rides on the streamed update). DocumentBlock.Kind / DocumentTableCell.Kind are now ChatRole-style open structs (DocumentBlockKind / DocumentTableCellKind), not raw strings. Added DocumentPage.Width / Height + DocumentCoordinateUnit so bounding-box coordinates are interpretable across engines. Removed the leaky DocumentExtractionResult.OcrSource (ModelId + DocumentExtractionClientMetadata.ProviderName carry provenance). Unsealed the result / data types to match ChatResponse / ChatOptions. Surface is now 29 public types.
2026-07-14 — Reformatted to the API-proposal template and refreshed the API Proposal speclet to match the surface implemented in PR Add IDocumentExtractionClient document-extraction capability as a new Microsoft.Extensions.DocumentExtraction library #7588: GetTextAsync → ExtractAsync; IDocumentExtractionClient : IDisposable; typed geometry DocumentPoint / DocumentBoundingBox with DocumentBoundingRegion.Polygon as IReadOnlyList<DocumentPoint>; 1-based DocumentPage.PageNumber; [Experimental("MEDE0001")]; added DocumentImage, DocumentPage.Images, DocumentExtractionOptions.Clone(), DocumentExtractionClientMetadata, the DocumentExtractionClientExtensions surface (incl. opt-in ExtractFromUriAsync), and the full builder / middleware / DI types. The sibling IDocumentAnalysisClient is scoped out to its own future proposal.
Background and motivation
Document parsing is a core RAG and ingestion building block, but there is no provider-neutral
document-extraction capability in the Microsoft.Extensions.* stack. Today, you either wire provider SDKs
directly or route OCR through IChatClient, which loses native document structure such as tables,
bounding boxes, confidence, polygons, and reading order.
This proposal adds IDocumentExtractionClient as a provider-neutral capability in its own peer
library, Microsoft.Extensions.DocumentExtraction — a sibling to Microsoft.Extensions.VectorData and Microsoft.Extensions.DataIngestion, not a capability inside Microsoft.Extensions.AI. It adopts the
same builder, middleware, and DI shape developers already use across the Microsoft.Extensions.AI
capability family, and references Microsoft.Extensions.AI.Abstractions internally (e.g. DataContent
inputs) without carrying the "AI" brand in its own namespace or types. Extraction pulls structured content out of documents; it complements DataIngestion, which feeds content into RAG.
Architecture at a glance
flowchart LR
subgraph Providers
DI[Azure Document Intelligence]
M[Mistral OCR]
CU[Content Understanding]
V[Vision-capable IChatClient adapter]
L[Local document model]
end
DI --> C
M --> C
CU --> C
V --> C
L --> C
C[IDocumentExtractionClient] --> D[Document pages, elements, tables, images, and geometry]
D --> MEDI[Microsoft.Extensions.DataIngestion]
D --> RAG[RAG and indexing pipelines]
D --> APP[Direct application consumers]
Loading
The client is the provider-neutral seam. The Document* types are the structured exchange model;
ingestion is one consumer of that model, not its owner or a required runtime.
What changed after the July review
Review concern
Evidence gathered
Current proposed design
Review status
OCR was too narrow and the M.E.AI assembly might be the wrong home
Shared-model and Azure Document Intelligence implementation spikes
12-engine output-model survey and MEDI consumer spike
One ordered DocumentPage.Elements list over DocumentElement
Implemented and evidence-validated
Close extensibility where the domain is bounded
Provider taxonomy and coordinate survey
Closed unit/origin enums; open block/cell kind structs
Implemented and evidence-validated
A document probably uses one coordinate unit
Google and Azure DI model units per page, including mixed image/PDF inputs
Dimensions, unit, and origin live on each DocumentPage
Implemented and evidence-validated
A shared document model needs an ownership story
The current MEDI bridge drops tables and typed geometry
Neutral Document* model lives here for v1; a later hoist remains additive
Confirmation requested
Escape hatches must earn their surface
Vision-adapter, metadata, SDK-escape, and 14-engine raw-output investigations
Keep GetService, RawRepresentation, and AdditionalProperties while experimental
Confirmation requested
What is OCR / document AI?
OCR is the process of extracting text from documents and images. Document AI goes further: it keeps
structure around that text, including pages, tables, blocks, regions, confidence scores, and reading
order.
For RAG and ingestion pipelines, that structure matters. A document reader should not only produce
markdown. It should also preserve enough page, region, table, confidence, and source metadata for
downstream chunking, retrieval, grounding, and evaluation.
Why an abstraction?
Microsoft.Extensions.AI.Abstractions ships a family of capability interfaces: IChatClient, IEmbeddingGenerator, ISpeechToTextClient, ITextToSpeechClient, IImageGenerator, IRealtimeClient, and IHostedFileClient. There is no OCR / document-extraction capability, even
though Microsoft.Extensions.DataIngestion (MEDI) already depends on document parsing. Its IngestionDocumentReader roadmap, per MS Learn, includes LlamaParse and Azure Document Intelligence,
both hosted document-AI services that need a provider-agnostic seam.
Today, if you want to use document-AI models, you must:
Couple ingestion code directly to a provider SDK.
Model OCR as a chat prompt against IChatClient.
Add reader-mode flags for specific engines or hosts.
Rebuild retry, logging, DI, middleware, and test seams per provider.
Give up native structure when the abstraction cannot represent it.
CommunityToolkit/AI #3 is a representative example.
It introduced a PdfReadingMode.VisionOnly flag that routes whole-document transcription through a
vision LLM (IChatClient). That is a layer leak: a model choice hardened into a reader-mode flag,
with temporal coupling because the reader emits placeholders that are useless unless a specific enricher
runs. The cleaner shape is a capability client the reader composes, exactly how MEDI's enrichers
already compose an injected IChatClient.
OCR is not chat. Most OCR / document-AI engines emit structured output: tables, bounding boxes,
confidence, polygons, reading order. That does not fit ChatResponse. A vision LLM can transcribe by
prompt, but it is the lowest-fidelity path and loses native structure. Purpose-built engines (Mistral
OCR, Azure Document Intelligence, Azure AI Content Understanding) and local document VLMs
(granite-docling, PaddleOCR) beat it. Modeling OCR as "call IChatClient with an image" makes those
engines unrepresentable without discarding their value. That points to a separate capability
interface, independent of IChatClient.
Prototype validation
This proposal is not a sketch. It describes a working prototype spiked across four real engine
providers (three using no IChatClient at all) plus one vision-LLM adapter, composed with an IChatClient-style builder pipeline, and validated end-to-end through a MEDI ingestion pipeline:
FoundryMistralDocumentExtractionClient: Azure AI Foundry mistral-ocr-4-0, keyless Entra (verified HTTP 200).
MistralDocumentExtractionClient: Mistral-direct, API key.
VisionLlmDocumentExtractionClient: the one adapter over IChatClient (gpt-4o / Gemini / local Ollama GLM-OCR), the lowest-fidelity path.
Every claim below ("one pipeline wraps all engines", "the polygon flows losslessly from DI and Mistral",
"streaming yields pages as they finish") is backed by code that builds and runs, not by assertion. The demo
is a public, runnable proof: one IDocumentExtractionClient in front of four OCR engines, bridged into a MEDI RAG
pipeline, with the identical consumer loop across both provider archetypes (document-native and
image-per-page).
The design goal is provider-neutrality: one small set of composable primitives that every provider
maps onto equally, judged on interoperability, reusability, composition, extensibility. No provider
is privileged. Providers form a coverage matrix, not a hierarchy.
The same precedent that justifies splitting OpenAIClient / AzureOpenAIClient behind IChatClient
applies here. The interface is the portability guarantee; concrete classes split by engine and by host where credential, route, or provider behavior leaks. The model/deployment id is a parameter,
never a type or a boolean flag.
Building-block symmetry with Microsoft.Extensions.AI
The goal is not a one-off OCR helper, and not a new pattern to learn. Microsoft.Extensions.DocumentExtraction
is a peer library that mirrors the M.E.AI building-block shape — exactly as VectorData and DataIngestion do — rather than a capability living inside M.E.AI.
If you know one Microsoft.Extensions.AI capability, you should know this one: abstraction,
options/result types, delegating base, builder/middleware, provider implementation, and DI registration.
The row below shows the shape it adopts.
Capability
Abstraction
Options / result types
Delegating base
Builder / middleware
Example implementation
DI registration shape
Chat
IChatClient
ChatOptions, ChatResponse, ChatResponseUpdate
DelegatingChatClient
ChatClientBuilder, .Use(...), logging, OpenTelemetry, caching, function invocation
That symmetry is the main API shape. IDocumentExtractionClient should feel like a natural next capability, not a
separate pattern you have to relearn.
Provider coverage
Providers are peers behind a provider-neutral contract, not a tier or hierarchy.
Provider
IDocumentExtractionClient (markdown/structure)
IDocumentAnalysisClient (typed fields + grounding) — future sibling proposal, not in this PR
Foundry Mistral OCR
yes
—
Azure Document Intelligence
yes
yes (Documents[].Fields)
Content Understanding
yes (markdown path)
yes (fields{} + grounding)
Vision-LLM adapter
yes (lowest fidelity)
—
Local ONNX / Ollama (roadmap)
yes
—
No row is privileged. Some providers implement more of the family than others; the family is the
design, and coverage is a matrix. Content Understanding is the widest-surface conformance test (one
service exercises both interfaces with the same primitives), not an apex; it validates
provider-neutrality because the same polygon / confidence / builder primitives serve its two shapes,
Mistral OCR, Azure DI, and a vision LLM. The second column previews a future sibling capability
(IDocumentAnalysisClient, see Related and future work) and is shown only to illustrate that the same
region / confidence / builder primitives generalize; it is not part of this PR.
API Proposal
The surface below is the current proposed public API on PR #7588 (32 public types), reshaped after the
2026-07-22 API review and a 12-engine provider survey. It is implemented and evidence-validated, but
remains subject to API review. Signatures only, no method bodies.
Delegating client, builder, extensions, logging, OpenTelemetry, options, DI
Standard composable capability plumbing
Core abstraction, options, and result types (Microsoft.Extensions.DocumentExtraction.Abstractions)
namespaceMicrosoft.Extensions.DocumentExtraction;/// <summary>/// A capability for OCR / document-extraction engines. Independent of <see cref="IChatClient"/>:/// engines emit structured output (tables, bounding boxes, confidence, reading order) that does not/// fit a chat response. One contract, many engines (Mistral OCR, Azure Document Intelligence, Content/// Understanding, a local ONNX model, or a vision LLM behind an adapter)./// </summary>[Experimental("MEDE0001")]publicinterfaceIDocumentExtractionClient:IDisposable{/// <summary>Runs OCR / document parsing over a document stream and returns structured text + pages.</summary>Task<DocumentExtractionResult>ExtractAsync(Streamdocument,stringmediaType,DocumentExtractionOptions?options=null,CancellationTokencancellationToken=default);/// <summary>/// Streams OCR / document parsing as <see cref="DocumentExtractionPageResult"/> values — one per page as it finishes/// (the <see cref="IChatClient.GetStreamingResponseAsync"/> twin). Reassemble into an/// <see cref="DocumentExtractionResult"/> via <see cref="DocumentExtractionPageResultExtensions.ToDocumentExtractionResultAsync"/>. Lets/// large-document RAG chunk/embed early pages while later pages are still being parsed./// </summary>IAsyncEnumerable<DocumentExtractionPageResult>ExtractPagesAsync(Streamdocument,stringmediaType,DocumentExtractionOptions?options=null,CancellationTokencancellationToken=default);/// <summary>Provider escape hatch (the <see cref="IChatClient.GetService"/> pattern).</summary>object?GetService(TypeserviceType,object?serviceKey=null);}/// <summary>Normalized OCR result — "normalize the common, preserve the raw" (the ChatResponse pattern).</summary>[Experimental("MEDE0001")]publicclassDocumentExtractionResult{publicDocumentExtractionResult(IReadOnlyList<DocumentPage>pages);publicIReadOnlyList<DocumentPage>Pages{get;}publicstringText{get;}// derived: page Text joined with blank linespublicDocumentExtractionUsage?Usage{get;set;}publicobject?RawRepresentation{get;set;}// provider-native object — nothing is lostpublicAdditionalPropertiesDictionary?AdditionalProperties{get;set;}}[Experimental("MEDE0001")]publicclassDocumentPage{publicDocumentPage(intpageNumber,stringtext);publicintPageNumber{get;}// 1-basedpublicstringText{get;}publicIReadOnlyList<DocumentElement>Elements{get;set;}// READING ORDER; OfType<T>() to project. default: emptypublicDocumentPageDimensions?Dimensions{get;set;}// page extent (width+height), when the engine reports itpublicDocumentCoordinateUnit?CoordinateUnit{get;set;}// per page — engines emit different units per page (image vs PDF batches)publicDocumentCoordinateOrigin?CoordinateOrigin{get;set;}[JsonIgnore]publicobject?RawRepresentation{get;set;}// provider-native page object; survives ToDocumentExtractionResult reduction (SPIKE-06)publicAdditionalPropertiesDictionary?AdditionalProperties{get;set;}}/// <summary>/// The reading-order element base — a polymorphic ($type) shape (the AIContent pattern), designed to be/// promotable to a future shared document-element type. DocumentBlock / DocumentTable / DocumentImage derive from it./// </summary>[Experimental("MEDE0001")][JsonPolymorphic(TypeDiscriminatorPropertyName="$type")][JsonDerivedType(typeof(DocumentBlock),"block")][JsonDerivedType(typeof(DocumentTable),"table")][JsonDerivedType(typeof(DocumentImage),"image")]publicabstractclassDocumentElement{protectedDocumentElement();publicDocumentBoundingRegion?BoundingRegion{get;set;}publicdouble?Confidence{get;set;}[JsonIgnore]publicobject?RawRepresentation{get;set;}publicAdditionalPropertiesDictionary?AdditionalProperties{get;set;}}[Experimental("MEDE0001")]publicclassDocumentBlock:DocumentElement{publicDocumentBlock(stringtext);publicstringText{get;}publicDocumentBlockKind?Kind{get;set;}// open struct: Paragraph / Title / Figure / ...// BoundingRegion, Confidence inherited from DocumentElement}/// <summary>An image or figure extracted from a page (emitted normally; no request flag).</summary>[Experimental("MEDE0001")]publicclassDocumentImage:DocumentElement{publicDataContent?Content{get;set;}publicstring?Caption{get;set;}// BoundingRegion, Confidence inherited from DocumentElement}/// <summary>A single 2-D point in page coordinates (a polygon vertex).</summary>[Experimental("MEDE0001")]publicreadonlyrecordstructDocumentPoint(floatX,floatY);/// <summary>An axis-aligned bounding box, for coarse filters / hit-testing.</summary>[Experimental("MEDE0001")]publicreadonlyrecordstructDocumentBoundingBox(floatLeft,floatTop,floatRight,floatBottom);/// <summary>/// The SHARED, provider-neutral geometry primitive — a polygon of DocumentPoint vertices (populated natively by/// Azure DI, via FromRectangle for Mistral's rect, and reused for field grounding). GetBounds() gives a coarse box./// </summary>[Experimental("MEDE0001")]publicclassDocumentBoundingRegion{publicDocumentBoundingRegion(intpageNumber,IReadOnlyList<DocumentPoint>polygon);publicintPageNumber{get;}publicIReadOnlyList<DocumentPoint>Polygon{get;}publicstaticDocumentBoundingRegionFromRectangle(intpageNumber,floatleft,floattop,floatright,floatbottom);// float (was double)publicDocumentBoundingBox?GetBounds();}/// <summary>Cells are the primary structured representation; MarkdownRepresentation is the fallback (Mistral).</summary>[Experimental("MEDE0001")]publicclassDocumentTable:DocumentElement{publicDocumentTable(introwCount,intcolumnCount,IReadOnlyList<DocumentTableCell>?cells=null,string?markdownRepresentation=null);publicintRowCount{get;}publicintColumnCount{get;}publicIReadOnlyList<DocumentTableCell>?Cells{get;}publicstring?MarkdownRepresentation{get;}// BoundingRegion, Confidence inherited from DocumentElement}[Experimental("MEDE0001")]publicclassDocumentTableCell{publicDocumentTableCell(introwIndex,intcolumnIndex,stringcontent);publicDocumentTableCellKind?Kind{get;set;}// open struct: ColumnHeader / Content / RowHeader / RowSectionpublicintRowIndex{get;}publicintColumnIndex{get;}publicintRowSpan{get;set;}// default 1publicintColumnSpan{get;set;}// default 1publicstringContent{get;}// flat-text convenience (kept)publicIReadOnlyList<DocumentElement>?Elements{get;set;}// optional NESTED content (structured cells)// Positioned-node facet mirrored from DocumentElement (NOT inheritance; reversible way-station, SPIKE-06):publicDocumentBoundingRegion?BoundingRegion{get;set;}// per-cell geometry (5 engines: Textract/Google/DI/Adobe/Docling)publicdouble?Confidence{get;set;}[JsonIgnore]publicobject?RawRepresentation{get;set;}publicAdditionalPropertiesDictionary?AdditionalProperties{get;set;}}/// <summary>/// A streamed OCR update — one completed page (the ChatResponseUpdate pattern). Reduce a sequence of these/// into an DocumentExtractionResult with DocumentExtractionPageResultExtensions. Page is non-null (no sentinel update)./// </summary>[Experimental("MEDE0001")]publicclassDocumentExtractionPageResult{[JsonConstructor]publicDocumentExtractionPageResult(DocumentPagepage);// Page is non-null (no sentinel update)publicDocumentPagePage{get;}publicint?PagesProcessed{get;set;}// progress (absorbs the retired DocumentExtractionProgress)publicint?TotalPages{get;set;}publicDocumentExtractionUsage?Usage{get;set;}[JsonIgnore]publicobject?RawRepresentation{get;set;}publicAdditionalPropertiesDictionary?AdditionalProperties{get;set;}}/// <summary>Reducers that assemble streamed page results back into one DocumentExtractionResult (the ToChatResponseAsync pattern).</summary>[Experimental("MEDE0001")]publicstaticclassDocumentExtractionPageResultExtensions{publicstaticDocumentExtractionResultToDocumentExtractionResult(thisIEnumerable<DocumentExtractionPageResult>updates);publicstaticTask<DocumentExtractionResult>ToDocumentExtractionResultAsync(thisIAsyncEnumerable<DocumentExtractionPageResult>updates,CancellationTokencancellationToken=default);}[Experimental("MEDE0001")]publicclassDocumentExtractionUsage{publicint?PagesProcessed{get;set;}publicint?InputTokenCount{get;set;}// vision-LLM path; classic OCR leaves these nullpublicint?OutputTokenCount{get;set;}publicint?TotalTokenCount{get;set;}publicAdditionalPropertiesDictionary?AdditionalProperties{get;set;}}/// <summary>The kind of a text block — a ChatRole-style OPEN set (well-knowns + provider-specific kinds).</summary>[Experimental("MEDE0001")]publicreadonlystructDocumentBlockKind:IEquatable<DocumentBlockKind>{publicDocumentBlockKind(stringvalue);// throws on null/whitespacepublicstaticDocumentBlockKindParagraph{get;}// "paragraph"publicstaticDocumentBlockKindTitle{get;}// "title"publicstaticDocumentBlockKindFigure{get;}// "figure"publicstringValue{get;}// == / != / IEquatable / GetHashCode / ToString + a JsonConverter (the ChatRole shape)}/// <summary>The kind of a table cell — a ChatRole-style OPEN set.</summary>[Experimental("MEDE0001")]publicreadonlystructDocumentTableCellKind:IEquatable<DocumentTableCellKind>{publicDocumentTableCellKind(stringvalue);publicstaticDocumentTableCellKindColumnHeader{get;}// "columnHeader"publicstaticDocumentTableCellKindContent{get;}// "content"publicstaticDocumentTableCellKindRowHeader{get;}// "rowHeader" (added)publicstaticDocumentTableCellKindRowSection{get;}// "rowSection" (added)publicstringValue{get;}}/// <summary>The unit for page dimensions + region coordinates — a CLOSED enum (units are physically bounded).</summary>[Experimental("MEDE0001")]publicenumDocumentCoordinateUnit{Pixel,Point,Inch,Normalized}/// <summary>Origin corner + y-axis direction of the coordinate space — a CLOSED enum.</summary>[Experimental("MEDE0001")]publicenumDocumentCoordinateOrigin{TopLeft,BottomLeft}/// <summary>Page extent (width + height), expressed in the page's DocumentCoordinateUnit — a readonly record struct (atomic pair).</summary>[Experimental("MEDE0001")]publicreadonlyrecordstructDocumentPageDimensions(floatWidth,floatHeight);/// <summary>Request knobs — the ChatOptions pattern.</summary>[Experimental("MEDE0001")]publicclassDocumentExtractionOptions{publicstring?ModelId{get;set;}// "GetChatClient(model)" analogpublicAdditionalPropertiesDictionary?AdditionalProperties{get;set;}publicDocumentExtractionOptionsClone();// shallow clone (the ChatOptions.Clone pattern)}/// <summary>Metadata about an <see cref="IDocumentExtractionClient"/> (the *ClientMetadata pattern).</summary>[Experimental("MEDE0001")]publicclassDocumentExtractionClientMetadata{publicDocumentExtractionClientMetadata(string?providerName=null,Uri?providerUri=null,string?defaultModelId=null);publicstring?ProviderName{get;}publicUri?ProviderUri{get;}publicstring?DefaultModelId{get;}}
/// <summary>Convenience helpers over <see cref="IDocumentExtractionClient"/>.</summary>[Experimental("MEDE0001")]publicstaticclassDocumentExtractionClientExtensions{publicstaticTService?GetService<TService>(thisIDocumentExtractionClientclient,object?serviceKey=null);// Extract from an in-memory DataContent (unary + streaming twin).publicstaticTask<DocumentExtractionResult>ExtractAsync(thisIDocumentExtractionClientclient,DataContentdocument,DocumentExtractionOptions?options=null,CancellationTokencancellationToken=default);publicstaticIAsyncEnumerable<DocumentExtractionPageResult>ExtractPagesAsync(thisIDocumentExtractionClientclient,DataContentdocument,DocumentExtractionOptions?options=null,CancellationTokencancellationToken=default);// Extract from a UriContent. Handles self-contained data: URIs; throws NotSupportedException for// file:/http(s) (whether to download vs. hand the URL to the engine is a deliberate non-decision).publicstaticTask<DocumentExtractionResult>ExtractAsync(thisIDocumentExtractionClientclient,UriContentdocument,DocumentExtractionOptions?options=null,CancellationTokencancellationToken=default);publicstaticIAsyncEnumerable<DocumentExtractionPageResult>ExtractPagesAsync(thisIDocumentExtractionClientclient,UriContentdocument,DocumentExtractionOptions?options=null,CancellationTokencancellationToken=default);// Explicit, opt-in remote downloader: fetches http(s) bytes with a caller-supplied HttpClient// (caller owns handlers/auth/timeouts/lifetime), inlines data: URIs, then extracts.publicstaticTask<DocumentExtractionResult>ExtractFromUriAsync(thisIDocumentExtractionClientclient,UriContentdocument,HttpClienthttpClient,DocumentExtractionOptions?options=null,CancellationTokencancellationToken=default);}
Delegating base, builder, middleware, and DI
All Microsoft.Extensions.AI capabilities ship the same five-layer shape: interface → Delegating<Cap> → <Cap>Builder → Add<Cap> (returns the builder) → .Use*()
middleware, with one composition primitive, Builder Use(Func<T, IServiceProvider, T>). IDocumentExtractionClient
mirrors it exactly.
// ---- Microsoft.Extensions.DocumentExtraction.Abstractions ----/// <summary>Optional base for an <see cref="IDocumentExtractionClient"/> that passes calls through to an inner instance.</summary>[Experimental("MEDE0001")]publicclassDelegatingDocumentExtractionClient:IDocumentExtractionClient{protectedDelegatingDocumentExtractionClient(IDocumentExtractionClientinnerClient);protectedIDocumentExtractionClientInnerClient{get;}publicvoidDispose();publicvirtualTask<DocumentExtractionResult>ExtractAsync(Streamdocument,stringmediaType,DocumentExtractionOptions?options=null,CancellationTokencancellationToken=default);publicvirtualIAsyncEnumerable<DocumentExtractionPageResult>ExtractPagesAsync(Streamdocument,stringmediaType,DocumentExtractionOptions?options=null,CancellationTokencancellationToken=default);publicvirtualobject?GetService(TypeserviceType,object?serviceKey=null);protectedvirtualvoidDispose(booldisposing);}// ---- Microsoft.Extensions.DocumentExtraction ----[Experimental("MEDE0001")]publicsealedclassDocumentExtractionClientBuilder{publicDocumentExtractionClientBuilder(IDocumentExtractionClientinnerClient);publicDocumentExtractionClientBuilder(Func<IServiceProvider,IDocumentExtractionClient>innerClientFactory);publicIDocumentExtractionClientBuild(IServiceProvider?services=null);// first .Use is outermostpublicDocumentExtractionClientBuilderUse(Func<IDocumentExtractionClient,IDocumentExtractionClient>clientFactory);publicDocumentExtractionClientBuilderUse(Func<IDocumentExtractionClient,IServiceProvider,IDocumentExtractionClient>clientFactory);// THE primitive}[Experimental("MEDE0001")]publicclassLoggingDocumentExtractionClient:DelegatingDocumentExtractionClient{}// logging middleware[Experimental("MEDE0001")]publicsealedclassOpenTelemetryDocumentExtractionClient:DelegatingDocumentExtractionClient{}// OTel middleware[Experimental("MEDE0001")]publicsealedclassConfigureOptionsDocumentExtractionClient:DelegatingDocumentExtractionClient{}// options middleware[Experimental("MEDE0001")]publicstaticclassDocumentExtractionClientBuilderExtensions{publicstaticDocumentExtractionClientBuilderAsBuilder(thisIDocumentExtractionClientinnerClient);}[Experimental("MEDE0001")]publicstaticclassLoggingDocumentExtractionClientBuilderExtensions{publicstaticDocumentExtractionClientBuilderUseLogging(thisDocumentExtractionClientBuilderbuilder,ILoggerFactory?loggerFactory=null,Action<LoggingDocumentExtractionClient>?configure=null);}[Experimental("MEDE0001")]publicstaticclassOpenTelemetryDocumentExtractionClientBuilderExtensions{publicstaticDocumentExtractionClientBuilderUseOpenTelemetry(thisDocumentExtractionClientBuilderbuilder,ILoggerFactory?loggerFactory=null,string?sourceName=null,Action<OpenTelemetryDocumentExtractionClient>?configure=null);}[Experimental("MEDE0001")]publicstaticclassConfigureOptionsDocumentExtractionClientBuilderExtensions{publicstaticDocumentExtractionClientBuilderConfigureOptions(thisDocumentExtractionClientBuilderbuilder,Action<DocumentExtractionOptions>configure);}[Experimental("MEDE0001")]publicstaticclassDocumentExtractionClientBuilderServiceCollectionExtensions{publicstaticDocumentExtractionClientBuilderAddDocumentExtractionClient(thisIServiceCollectionserviceCollection,IDocumentExtractionClientinnerClient,ServiceLifetimelifetime=ServiceLifetime.Singleton);publicstaticDocumentExtractionClientBuilderAddDocumentExtractionClient(thisIServiceCollectionserviceCollection,Func<IServiceProvider,IDocumentExtractionClient>innerClientFactory,ServiceLifetimelifetime=ServiceLifetime.Singleton);publicstaticDocumentExtractionClientBuilderAddKeyedDocumentExtractionClient(thisIServiceCollectionserviceCollection,object?serviceKey,IDocumentExtractionClientinnerClient,ServiceLifetimelifetime=ServiceLifetime.Singleton);publicstaticDocumentExtractionClientBuilderAddKeyedDocumentExtractionClient(thisIServiceCollectionserviceCollection,object?serviceKey,Func<IServiceProvider,IDocumentExtractionClient>innerClientFactory,ServiceLifetimelifetime=ServiceLifetime.Singleton);}
IDocumentExtractionClient ships logging, OpenTelemetry, and configure-options middleware in v1, matching the ISpeechToTextClient template. It does not ship a built-in retry client: no Microsoft.Extensions.AI
capability does, because resilience belongs in the HTTP pipeline
(Microsoft.Extensions.Http.Resilience). The .Use(...) primitive still lets a consumer wrap a custom
retry or cache decorator when they want one.
API Usage
Every snippet below is drawn from the runnable iocrclient-demo, which exercises this exact
surface across four engines and a MEDI RAG pipeline.
One interface, four engines — the payoff. Vision LLM, Mistral OCR, Azure Document Intelligence, and
Azure Content Understanding each speak a completely different wire protocol. Behind IDocumentExtractionClient they are IDocumentExtractionClient; the consumer loop never changes when you add or swap a provider.
varclients=new(stringName,IDocumentExtractionClientClient)[]{("vision-llm",newVisionLlmDocumentExtractionClient(chatClient)),("mistral-ocr",newFoundryMistralDocumentExtractionClient(foundryEndpoint,cred)),("azure-document-intelligence",newAzureDocumentIntelligenceClient(diEndpoint,cred)),("azure-content-understanding",newContentUnderstandingClient(cuEndpoint,cred)),};byte[]bytes=awaitFile.ReadAllBytesAsync("report.pdf");foreach(var(name,client)inclients){using(client){usingvarstream=newMemoryStream(bytes,writable:false);DocumentExtractionResultr=awaitclient.ExtractAsync(stream,"application/pdf");// identical for every engineinttables=r.Pages.Sum(p =>p.Elements.OfType<DocumentTable>().Count());Console.WriteLine($"{name}: {r.Pages.Count} pages, {tables} tables, {r.Text.Length} chars");}}
Stream pages as they finish — the IChatClient.GetStreamingResponseAsync twin. Each DocumentExtractionPageResult carries one completed page (plus progress + usage), so a RAG pipeline can chunk and
embed early pages while later pages are still being OCR'd; ToDocumentExtractionResultAsync reduces the stream back to
the same DocumentExtractionResult the unary call would return.
awaitforeach(DocumentExtractionPageResultupdateinclient.ExtractPagesAsync(stream,"application/pdf")){DocumentPagepage=update.Page;Console.WriteLine($"page {page.PageNumber}/{update.TotalPages}: {page.Text.Length} chars");}// …or reduce the whole stream back into one DocumentExtractionResult (the ToChatResponseAsync pattern):DocumentExtractionResultfull=awaitclient.ExtractPagesAsync(stream,"application/pdf").ToDocumentExtractionResultAsync();
Consume elements in reading order. A page has one ordered stream rather than parallel collections
that require consumers to reconstruct order from geometry.
Interpret geometry in the page's own coordinate system. Unit and origin are page-scoped because
one request can contain image pages measured in pixels and PDF pages measured in inches or points.
DocumentExtractionResultresult=awaitclient.ExtractAsync(stream,"application/pdf");foreach(DocumentPagepageinresult.Pages){if(page.Dimensionsis{}size&&page.CoordinateUnitis{}unit&&page.CoordinateOriginis{}origin){Console.WriteLine($"Page {page.PageNumber}: {size.Width} x {size.Height}{unit}, origin {origin}");foreach(DocumentElementelementinpage.Elements){DocumentBoundingBox?bounds=element.BoundingRegion?.GetBounds();// Normalize or transform bounds using this page's size, unit, and origin.}}}
Compose middleware with the builder — the same shape as ChatClientBuilder: you compose a client,
you don't set flags.
Discover optional services without widening the core contract. Middleware can inspect provider
metadata, and an opaque adapter can expose the client it wraps.
staticvoidInspect(IDocumentExtractionClientclient){DocumentExtractionClientMetadata?metadata=client.GetService<DocumentExtractionClientMetadata>();IChatClient?innerChatClient=client.GetService<IChatClient>();// non-null when an adapter chooses to expose it}
Register with dependency injection — the consumer depends only on IDocumentExtractionClient; swap the engine
without touching downstream code.
services.AddDocumentExtractionClient(sp =>newFoundryMistralDocumentExtractionClient(endpoint,newDefaultAzureCredential())).UseOpenTelemetry().UseLogging();// Later, swap the engine on one line — nothing downstream changes:services.AddDocumentExtractionClient(sp =>newAzureDocumentIntelligenceClient(diEndpoint,cred)).UseLogging();
Configure per-request options through the pipeline (for example, a local Ollama GLM-OCR engine that
needs a task-prefix prompt injected even when the caller passes no options):
Bridge into a MEDI ingestion / RAG pipeline. A hosted document-AI service is a reader (the
LlamaParse-as-reader shape). One provider-agnostic reader composes any IDocumentExtractionClient:
publicsealedclassOcrDocumentReader(IDocumentExtractionClientocr,DocumentExtractionOptions?options=null):IngestionDocumentReader{publicoverrideasyncTask<IngestionDocument>ReadAsync(Streamsource,stringidentifier,stringmediaType,CancellationTokenct=default){DocumentExtractionResultr=awaitocr.ExtractAsync(source,mediaType,options,ct);// map r.Pages -> IngestionDocument elements, stamping page/region/confidence/model metadata}}// Usage: one section per OCR page, each stamped with its 1-based PageNumber for downstream chunking.varreader=newOcrDocumentReader(ocr);IngestionDocumentdoc=awaitreader.ReadAsync(fileStream,"report.pdf","application/pdf");
Extract from a remote URI (opt-in download).ExtractAsync(UriContent) never touches the network; ExtractFromUriAsync is the explicit counterpart that fetches http(s) bytes with a caller-owned HttpClient:
Reuse IChatClient with multimodal content + a prompt. Rejected: loses native
tables/bbox/confidence, is nondeterministic and token-expensive, and cannot represent non-chat engines
(Azure DI, CU, local ONNX) at all. A vision LLM is supported as one provider behindIDocumentExtractionClient
(VisionLlmDocumentExtractionClient), not as the contract. The prototype demonstrates this: three of four real
engines use no IChatClient.
A flag on the reader (the VisionOnly approach). Rejected: a model choice hardened into a reader
mode; temporal coupling; not decoratable (no middleware); does not generalize across engines.
A transport-parameterized single class (new MistralDocumentExtractionClient(isFoundry: true)). Rejected:
smuggles host branching into the type; breaks composition. Follow the OpenAIClient/AzureOpenAIClient
precedent: the interface is portable; concrete classes split by host where the host leaks.
One interface for OCR and field extraction (a flag/overload). Rejected: different output
contract; modeled as the sibling IDocumentAnalysisClient peer (the STT/TTS precedent). See Future sibling: IDocumentAnalysisClient below.
Validated design decisions and evidence
The table distinguishes implementation and evidence from review approval. “Implemented and
evidence-validated” means the shape exists in #7588 and survived the provider/prototype work; it does
not mean the API has already been approved.
Decision
Evidence
Current proposed resolution
Status
Structured geometry
Azure DI emits rotation/skew-capable polygons; Mistral emits rectangles
Typed DocumentPoint polygon with GetBounds() for coarse boxes
Implemented and evidence-validated
Structured tables
Azure DI exposes cells while Mistral can provide a markdown fallback
DocumentTable with cells plus optional markdown representation
Implemented and evidence-validated
Page identity
Documents, Azure DI, and downstream provenance use one-based pages
DocumentPage.PageNumber is one-based
Implemented and evidence-validated
Unary and streaming pair
Large documents benefit from early-page processing; target frameworks prevent adding a default interface member later
ExtractAsync plus page-at-a-time ExtractPagesAsync; reducers rebuild a full result
Implemented and evidence-validated
Reading order
Surveyed engines emit a heterogeneous ordered stream; MEDI consumers otherwise reconstruct order from geometry
One DocumentPage.Elements list over DocumentElement, including nested cell elements
Implemented and evidence-validated
Open versus closed vocabularies
Coordinate units/origins are physically bounded; provider block/cell taxonomies are large and evolving
Closed coordinate enums; open DocumentBlockKind and DocumentTableCellKind structs
Implemented and evidence-validated
Coordinate ownership
Google and Azure DI report dimensions/unit per page; mixed image/PDF inputs can use different units
Dimensions, CoordinateUnit, and CoordinateOrigin live on each page
Implemented and evidence-validated
Text naming
Many engines return plain text rather than genuine Markdown
Text is the common property; Markdown can be additive later
Implemented and evidence-validated
Usage
Vision adapters report tokens; classic engines generally report page progress
Optional token counts in DocumentExtractionUsage; page progress stays on page results
Implemented and evidence-validated
Package and naming family
Shared-model and Azure DI spikes both pointed away from the M.E.AI assembly
Standalone DocumentExtraction peer library; neutral Document* model and DocumentExtraction* operations
Confirmation requested
Shared-model home
The existing MEDI bridge drops tables and typed geometry
Keep the neutral model here for v1; preserve an additive future hoist
Confirmation requested
Escape hatches
Adapter unwrapping, metadata discovery, provider SDK access, and a 14-engine raw-output inventory
Keep GetService, RawRepresentation, and AdditionalProperties while experimental
Confirmation requested
Decisions requested in this review
Library boundary and name: Confirm Microsoft.Extensions.DocumentExtraction(.Abstractions) as the standalone peer library, or name
the specific replacement.
Model home: Confirm that the neutral Document* model can live in this package for v1, with a
later namespace/package hoist remaining additive.
Escape hatches: Confirm GetService, RawRepresentation, and AdditionalProperties, or
identify the unsupported scenario or replacement API for each removed seam.
V1 composition surface: Confirm builder, delegating client, DI, logging, OpenTelemetry, and
configure-options; the recommendation is to defer built-in caching and retry policy.
Collection mutability: Confirm settable IReadOnlyList properties for provider population, or
request a constructor/init-only alternative compatible with the supported target frameworks.
These items are intentionally separated from the current API decision. Reviewers can promote one to a
v1 blocker, but the proposal does not assume that on their behalf.
Follow-up
Current disposition
IChatClient vision-capability metadata
Cross-team M.E.AI discussion; the document-extraction contract does not depend on the outcome
New C# extension-member syntax
Verify repository language, analyzer, and public-API baseline support before adoption
Distributed caching
Proposed defer; compose later through the existing builder seam when a concrete policy is ready
Retry
Provider/transport concern handled through HTTP resilience or a custom delegating client
Multi-image or batch-page input
Additive follow-up; callers can render/split upstream and invoke once per image today
Consumer-ergonomics sample beyond this issue
Add a runnable reading-order consumer, but do not block the public contract on another sample
Shared model hoist
Revisit when another ready capability and owner require a common package
Azure Document Intelligence provider package
Provider-team-owned satellite implementation; direct implementation is already proven feasible
Page versus chunk
Page is the v1 streaming unit; revisit only if a future shared model establishes a different stable unit
Region primitive name
Keep DocumentBoundingRegion for v1; reconsider a more neutral name only with a second capability
IDocumentAnalysisClient
Separate proposal because schema-based field extraction has a different output contract
Future sibling: IDocumentAnalysisClient
Field extraction takes a document plus a schema/analyzer and returns typed fields with confidence and
grounding. That is categorically different from extracting a provider-neutral document structure, so
it should be a peer interface rather than a mode, flag, or overload on IDocumentExtractionClient.
The future sibling may reuse DocumentBoundingRegion and the builder pattern, but its schema,
typed-field value model, and lifecycle require their own proposal and provider matrix.
Review provenance
How the July review and follow-up spikes produced the current shape
Review or spike
Outcome in the current proposal
Rename streaming operation and page result
ExtractPagesAsync and DocumentExtractionPageResult
Replace parallel content lists
Ordered DocumentPage.Elements hierarchy
Shared document-model investigation
Standalone peer library with a neutral Document* model
Nested table-cell content
Optional DocumentTableCell.Elements
Vision adapter and GetService investigation
Keep one optional service/unwrap seam
Raw-output investigation across 14 engines
Keep raw/properties; selectively promote repeated common fields
Page dimensions and coordinate survey
Per-page dimensions, unit, and origin
BCL geometry investigation
Keep document-owned polygon primitives; no cross-platform BCL polygon fits
Azure Document Intelligence adapter spike
Direct implementation proven; provider package remains separate
Family rename
Ocr* becomes Document* / DocumentExtraction* after leaving M.E.AI
The full SPIKE-01..13 ledger, provider survey, raw-output inventory, and ADRs remain the durable
provenance record. They should be linked from the review handoff where those artifacts are published,
rather than reproduced line-by-line in this issue.
References
CommunityToolkit/AI #3: document-processing
packages and the VisionOnly reader flag this proposal replaces.
iocrclient-demo: runnable provider and MEDI
coverage prototype.
Risks
Risk
Mitigation / review question
Permanent surface area: 32 public types are a meaningful compatibility commitment
The surface is [Experimental]; retain a type only when a demonstrated cross-provider scenario requires it
Shape creep: extraction could absorb analysis, conversion, batching, caching, or provider policy
Keep field analysis in a sibling proposal and keep orchestration/policy at the edges
Package/model ownership: evidence supports the standalone home, but governance has not been explicitly confirmed
Make package name and v1 model home explicit review decisions
Escape-hatch misuse: raw objects and additional properties can become an alternative untyped API
Normalize repeated common concepts; reserve the hatches for provider-specific data and document that guidance
Mutable result collections: settable IReadOnlyList properties aid provider construction but may surprise consumers
Decide in review whether setters, init-only properties, or constructor population best fit the target frameworks
Provider input differences: document-native and image-per-page engines accept different input strategies
Keep one document stream as the narrow contract; leave rendering, batching, and windowing to additive edge APIs
Middleware scope: caching or resilience can expand the first release without proving the core capability
Ship standard composition seams; defer policy-specific middleware until a concrete scenario and design are ready
Landing criterion
The proposal is ready for the next gate when each confirmation request has an explicit disposition,
every blocking change has an owner, and deferred ideas are not treated as hidden prerequisites.
[API Proposal]:
IDocumentExtractionClient— a document-extraction capability as its own peer library (Microsoft.Extensions.DocumentExtraction)Important
Review status: This proposal describes the current 32-type
[Experimental]surface implementedby #7588. The prototype, provider surveys, and implementation validate that the shape is feasible;
they do not replace reviewer approval.
Confirmation requested in this review: the standalone
Microsoft.Extensions.DocumentExtraction(.Abstractions)home and name; the neutralDocument*model living there for v1; the
GetService/RawRepresentation/AdditionalPropertiesposture;the v1 middleware boundary; and the next formal approval gate.
Implementation status (2026-08-10): #7588 is open and non-draft at
a215825ae2; its Ubuntu,Windows, coverage, aggregate CI, and CLA checks are green.
Proposal change history and superseded intermediate states
Background and motivation
Document parsing is a core RAG and ingestion building block, but there is no provider-neutral
document-extraction capability in the
Microsoft.Extensions.*stack. Today, you either wire provider SDKsdirectly or route OCR through
IChatClient, which loses native document structure such as tables,bounding boxes, confidence, polygons, and reading order.
This proposal adds
IDocumentExtractionClientas a provider-neutral capability in its own peerlibrary,
Microsoft.Extensions.DocumentExtraction— a sibling toMicrosoft.Extensions.VectorDataandMicrosoft.Extensions.DataIngestion, not a capability insideMicrosoft.Extensions.AI. It adopts thesame builder, middleware, and DI shape developers already use across the Microsoft.Extensions.AI
capability family, and references
Microsoft.Extensions.AI.Abstractionsinternally (e.g.DataContentinputs) without carrying the "AI" brand in its own namespace or types. Extraction pulls structured content
out of documents; it complements
DataIngestion, which feeds content into RAG.Architecture at a glance
flowchart LR subgraph Providers DI[Azure Document Intelligence] M[Mistral OCR] CU[Content Understanding] V[Vision-capable IChatClient adapter] L[Local document model] end DI --> C M --> C CU --> C V --> C L --> C C[IDocumentExtractionClient] --> D[Document pages, elements, tables, images, and geometry] D --> MEDI[Microsoft.Extensions.DataIngestion] D --> RAG[RAG and indexing pipelines] D --> APP[Direct application consumers]The client is the provider-neutral seam. The
Document*types are the structured exchange model;ingestion is one consumer of that model, not its owner or a required runtime.
What changed after the July review
Microsoft.Extensions.DocumentExtraction(.Abstractions)peer libraryDocumentPage.Elementslist overDocumentElementDocumentPageDocument*model lives here for v1; a later hoist remains additiveGetService,RawRepresentation, andAdditionalPropertieswhile experimentalWhat is OCR / document AI?
OCR is the process of extracting text from documents and images. Document AI goes further: it keeps
structure around that text, including pages, tables, blocks, regions, confidence scores, and reading
order.
For RAG and ingestion pipelines, that structure matters. A document reader should not only produce
markdown. It should also preserve enough page, region, table, confidence, and source metadata for
downstream chunking, retrieval, grounding, and evaluation.
Why an abstraction?
Microsoft.Extensions.AI.Abstractionsships a family of capability interfaces:IChatClient,IEmbeddingGenerator,ISpeechToTextClient,ITextToSpeechClient,IImageGenerator,IRealtimeClient, andIHostedFileClient. There is no OCR / document-extraction capability, eventhough
Microsoft.Extensions.DataIngestion(MEDI) already depends on document parsing. ItsIngestionDocumentReaderroadmap, per MS Learn, includes LlamaParse and Azure Document Intelligence,both hosted document-AI services that need a provider-agnostic seam.
Today, if you want to use document-AI models, you must:
IChatClient.CommunityToolkit/AI #3 is a representative example.
It introduced a
PdfReadingMode.VisionOnlyflag that routes whole-document transcription through avision LLM (
IChatClient). That is a layer leak: a model choice hardened into a reader-mode flag,with temporal coupling because the reader emits placeholders that are useless unless a specific enricher
runs. The cleaner shape is a capability client the reader composes, exactly how MEDI's enrichers
already compose an injected
IChatClient.OCR is not chat. Most OCR / document-AI engines emit structured output: tables, bounding boxes,
confidence, polygons, reading order. That does not fit
ChatResponse. A vision LLM can transcribe byprompt, but it is the lowest-fidelity path and loses native structure. Purpose-built engines (Mistral
OCR, Azure Document Intelligence, Azure AI Content Understanding) and local document VLMs
(granite-docling, PaddleOCR) beat it. Modeling OCR as "call
IChatClientwith an image" makes thoseengines unrepresentable without discarding their value. That points to a separate capability
interface, independent of
IChatClient.Prototype validation
This proposal is not a sketch. It describes a working prototype spiked across four real engine
providers (three using no
IChatClientat all) plus one vision-LLM adapter, composed with anIChatClient-style builder pipeline, and validated end-to-end through a MEDI ingestion pipeline:FoundryMistralDocumentExtractionClient: Azure AI Foundrymistral-ocr-4-0, keyless Entra (verified HTTP 200).MistralDocumentExtractionClient: Mistral-direct, API key.AzureDocumentIntelligenceClient:Azure.AI.DocumentIntelligence(AnalyzeResult, native polygons + table cells).ContentUnderstandingClient:Azure.AI.ContentUnderstanding1.1.0, keyless Entra (markdown path).VisionLlmDocumentExtractionClient: the one adapter overIChatClient(gpt-4o / Gemini / local Ollama GLM-OCR), the lowest-fidelity path.Every claim below ("one pipeline wraps all engines", "the polygon flows losslessly from DI and Mistral",
"streaming yields pages as they finish") is backed by code that builds and runs, not by assertion. The demo
is a public, runnable proof: one
IDocumentExtractionClientin front of four OCR engines, bridged into a MEDI RAGpipeline, with the identical consumer loop across both provider archetypes (document-native and
image-per-page).
The design goal is provider-neutrality: one small set of composable primitives that every provider
maps onto equally, judged on interoperability, reusability, composition, extensibility. No provider
is privileged. Providers form a coverage matrix, not a hierarchy.
The same precedent that justifies splitting
OpenAIClient/AzureOpenAIClientbehindIChatClientapplies here. The interface is the portability guarantee; concrete classes split by engine and by
host where credential, route, or provider behavior leaks. The model/deployment id is a parameter,
never a type or a boolean flag.
Building-block symmetry with Microsoft.Extensions.AI
The goal is not a one-off OCR helper, and not a new pattern to learn.
Microsoft.Extensions.DocumentExtractionis a peer library that mirrors the M.E.AI building-block shape — exactly as
VectorDataandDataIngestiondo — rather than a capability living inside M.E.AI.If you know one Microsoft.Extensions.AI capability, you should know this one: abstraction,
options/result types, delegating base, builder/middleware, provider implementation, and DI registration.
The row below shows the shape it adopts.
IChatClientChatOptions,ChatResponse,ChatResponseUpdateDelegatingChatClientChatClientBuilder,.Use(...), logging, OpenTelemetry, caching, function invocationOpenAIChatClientAddChatClient,AddKeyedChatClientIEmbeddingGenerator<TInput,TEmbedding>EmbeddingGenerationOptions,GeneratedEmbeddings<TEmbedding>DelegatingEmbeddingGenerator<TInput,TEmbedding>EmbeddingGeneratorBuilder<TInput,TEmbedding>,.Use(...), logging, OpenTelemetry, cachingOpenAIEmbeddingGeneratorAddEmbeddingGenerator,AddKeyedEmbeddingGeneratorISpeechToTextClientSpeechToTextOptions,SpeechToTextResponse, response updatesDelegatingSpeechToTextClientSpeechToTextClientBuilder,.Use(...), logging, OpenTelemetry, optionsOpenAISpeechToTextClientAddSpeechToTextClient,AddKeyedSpeechToTextClientITextToSpeechClientTextToSpeechOptions,TextToSpeechResponse, response updatesDelegatingTextToSpeechClientTextToSpeechClientBuilder,.Use(...), logging, OpenTelemetry, optionsOpenAITextToSpeechClientAddTextToSpeechClient,AddKeyedTextToSpeechClientIImageGeneratorImageGenerationOptions,ImageGenerationRequest,ImageGenerationResponseDelegatingImageGeneratorImageGeneratorBuilder,.Use(...), logging, optionsOpenAIImageGeneratorAddImageGenerator,AddKeyedImageGeneratorIRealtimeClientRealtimeSessionOptions, client/server messages, sessionsDelegatingRealtimeClientRealtimeClientBuilder,.Use(...), logging, OpenTelemetry, function invocationOpenAIRealtimeClientIRealtimeClient/ keyed clients through DIIHostedFileClientHostedFileClientOptions,HostedFileDownloadStreamDelegatingHostedFileClientHostedFileClientBuilder,.Use(...), logging, OpenTelemetryOpenAIHostedFileClientIHostedFileClient/ keyed clients through DIIDocumentExtractionClientDocumentExtractionOptions,DocumentExtractionResult,DocumentExtractionPageResult,DocumentPage,DocumentBlock,DocumentTable,DocumentImage,DocumentExtractionUsageDelegatingDocumentExtractionClientDocumentExtractionClientBuilder,.Use(...), logging, OpenTelemetry, configure-optionsFoundryMistralDocumentExtractionClient,MistralDocumentExtractionClient,AzureDocumentIntelligenceClient,ContentUnderstandingClient,VisionLlmDocumentExtractionClientAddDocumentExtractionClient,AddKeyedDocumentExtractionClientThat symmetry is the main API shape.
IDocumentExtractionClientshould feel like a natural next capability, not aseparate pattern you have to relearn.
Provider coverage
Providers are peers behind a provider-neutral contract, not a tier or hierarchy.
IDocumentExtractionClient(markdown/structure)IDocumentAnalysisClient(typed fields + grounding) — future sibling proposal, not in this PRDocuments[].Fields)fields{}+ grounding)No row is privileged. Some providers implement more of the family than others; the family is the
design, and coverage is a matrix. Content Understanding is the widest-surface conformance test (one
service exercises both interfaces with the same primitives), not an apex; it validates
provider-neutrality because the same polygon / confidence / builder primitives serve its two shapes,
Mistral OCR, Azure DI, and a vision LLM. The second column previews a future sibling capability
(
IDocumentAnalysisClient, see Related and future work) and is shown only to illustrate that the sameregion / confidence / builder primitives generalize; it is not part of this PR.
API Proposal
The surface below is the current proposed public API on PR #7588 (32 public types), reshaped after the
2026-07-22 API review and a 12-engine provider survey. It is implemented and evidence-validated, but
remains subject to API review. Signatures only, no method bodies.
IDocumentExtractionClient, options, result, page result, usageDocumentPageandDocumentElementhierarchyCore abstraction, options, and result types (
Microsoft.Extensions.DocumentExtraction.Abstractions)Extension methods (
DocumentExtractionClientExtensions)Delegating base, builder, middleware, and DI
All Microsoft.Extensions.AI capabilities ship the same five-layer shape:
interface→Delegating<Cap>→<Cap>Builder→Add<Cap>(returns the builder) →.Use*()middleware, with one composition primitive,
Builder Use(Func<T, IServiceProvider, T>).IDocumentExtractionClientmirrors it exactly.
IDocumentExtractionClientships logging, OpenTelemetry, and configure-options middleware in v1, matching theISpeechToTextClienttemplate. It does not ship a built-in retry client: noMicrosoft.Extensions.AIcapability does, because resilience belongs in the HTTP pipeline
(
Microsoft.Extensions.Http.Resilience). The.Use(...)primitive still lets a consumer wrap a customretry or cache decorator when they want one.
API Usage
Every snippet below is drawn from the runnable
iocrclient-demo, which exercises this exactsurface across four engines and a MEDI RAG pipeline.
One interface, four engines — the payoff. Vision LLM, Mistral OCR, Azure Document Intelligence, and
Azure Content Understanding each speak a completely different wire protocol. Behind
IDocumentExtractionClientthey areIDocumentExtractionClient; the consumer loop never changes when you add or swap a provider.Stream pages as they finish — the
IChatClient.GetStreamingResponseAsynctwin. EachDocumentExtractionPageResultcarries one completed page (plus progress + usage), so a RAG pipeline can chunk andembed early pages while later pages are still being OCR'd;
ToDocumentExtractionResultAsyncreduces the stream back tothe same
DocumentExtractionResultthe unary call would return.Consume elements in reading order. A page has one ordered stream rather than parallel collections
that require consumers to reconstruct order from geometry.
Interpret geometry in the page's own coordinate system. Unit and origin are page-scoped because
one request can contain image pages measured in pixels and PDF pages measured in inches or points.
Compose middleware with the builder — the same shape as
ChatClientBuilder: you compose a client,you don't set flags.
Discover optional services without widening the core contract. Middleware can inspect provider
metadata, and an opaque adapter can expose the client it wraps.
Register with dependency injection — the consumer depends only on
IDocumentExtractionClient; swap the enginewithout touching downstream code.
Configure per-request options through the pipeline (for example, a local Ollama GLM-OCR engine that
needs a task-prefix prompt injected even when the caller passes no options):
Bridge into a MEDI ingestion / RAG pipeline. A hosted document-AI service is a reader (the
LlamaParse-as-reader shape). One provider-agnostic reader composes any
IDocumentExtractionClient:Extract from a remote URI (opt-in download).
ExtractAsync(UriContent)never touches the network;ExtractFromUriAsyncis the explicit counterpart that fetches http(s) bytes with a caller-ownedHttpClient:Alternative Designs
IChatClientwith multimodal content + a prompt. Rejected: loses nativetables/bbox/confidence, is nondeterministic and token-expensive, and cannot represent non-chat engines
(Azure DI, CU, local ONNX) at all. A vision LLM is supported as one provider behind
IDocumentExtractionClient(
VisionLlmDocumentExtractionClient), not as the contract. The prototype demonstrates this: three of four realengines use no
IChatClient.VisionOnlyapproach). Rejected: a model choice hardened into a readermode; temporal coupling; not decoratable (no middleware); does not generalize across engines.
new MistralDocumentExtractionClient(isFoundry: true)). Rejected:smuggles host branching into the type; breaks composition. Follow the
OpenAIClient/AzureOpenAIClientprecedent: the interface is portable; concrete classes split by host where the host leaks.
contract; modeled as the sibling
IDocumentAnalysisClientpeer (the STT/TTS precedent). See Future sibling:IDocumentAnalysisClientbelow.Validated design decisions and evidence
The table distinguishes implementation and evidence from review approval. “Implemented and
evidence-validated” means the shape exists in #7588 and survived the provider/prototype work; it does
not mean the API has already been approved.
DocumentPointpolygon withGetBounds()for coarse boxesDocumentTablewith cells plus optional markdown representationDocumentPage.PageNumberis one-basedExtractAsyncplus page-at-a-timeExtractPagesAsync; reducers rebuild a full resultDocumentPage.Elementslist overDocumentElement, including nested cell elementsDocumentBlockKindandDocumentTableCellKindstructsDimensions,CoordinateUnit, andCoordinateOriginlive on each pageTextis the common property; Markdown can be additive laterDocumentExtractionUsage; page progress stays on page resultsDocumentExtractionpeer library; neutralDocument*model andDocumentExtraction*operationsGetService,RawRepresentation, andAdditionalPropertieswhile experimentalDecisions requested in this review
Microsoft.Extensions.DocumentExtraction(.Abstractions)as the standalone peer library, or namethe specific replacement.
Document*model can live in this package for v1, with alater namespace/package hoist remaining additive.
GetService,RawRepresentation, andAdditionalProperties, oridentify the unsupported scenario or replacement API for each removed seam.
configure-options; the recommendation is to defer built-in caching and retry policy.
IReadOnlyListproperties for provider population, orrequest a constructor/init-only alternative compatible with the supported target frameworks.
approval and Add IDocumentExtractionClient document-extraction capability as a new Microsoft.Extensions.DocumentExtraction library #7588 review.
Deferred follow-ups, not part of this proposal
These items are intentionally separated from the current API decision. Reviewers can promote one to a
v1 blocker, but the proposal does not assume that on their behalf.
IChatClientvision-capability metadataDocumentBoundingRegionfor v1; reconsider a more neutral name only with a second capabilityIDocumentAnalysisClientFuture sibling:
IDocumentAnalysisClientField extraction takes a document plus a schema/analyzer and returns typed fields with confidence and
grounding. That is categorically different from extracting a provider-neutral document structure, so
it should be a peer interface rather than a mode, flag, or overload on
IDocumentExtractionClient.The future sibling may reuse
DocumentBoundingRegionand the builder pattern, but its schema,typed-field value model, and lifecycle require their own proposal and provider matrix.
Review provenance
How the July review and follow-up spikes produced the current shape
ExtractPagesAsyncandDocumentExtractionPageResultDocumentPage.ElementshierarchyDocument*modelDocumentTableCell.ElementsGetServiceinvestigationOcr*becomesDocument*/DocumentExtraction*after leaving M.E.AIThe full SPIKE-01..13 ledger, provider survey, raw-output inventory, and ADRs remain the durable
provenance record. They should be linked from the review handoff where those artifacts are published,
rather than reproduced line-by-line in this issue.
References
packages and the
VisionOnlyreader flag this proposal replaces.area-data-ingestionIngestionPipeline (#7488):a downstream consumer of a provider-neutral document reader.
IVideoGeneratorproposal (#7420):capability-family precedents.
iocrclient-demo: runnable provider and MEDIcoverage prototype.
Risks
[Experimental]; retain a type only when a demonstrated cross-provider scenario requires itIReadOnlyListproperties aid provider construction but may surprise consumersLanding criterion
The proposal is ready for the next gate when each confirmation request has an explicit disposition,
every blocking change has an owner, and deferred ideas are not treated as hidden prerequisites.