Skip to main content

nfield

Pull N structured fields out of a document, reliably.

From a handful of fields to thousands, on models that choke on a single-call request.

PyPI Python CI Typed License

Quickstart · Benchmarks · CLI · Docs · How it works


Ask an LLM to fill one big JSON schema in a single call and the answers get worse as the schema grows. The model spends its output budget on brackets, commas, and quotes instead of values, and a wide schema overflows the context window before it finishes. Most structured-output tools are built for a handful of fields and quietly fall apart past that.

nfield is built for the wide case. It splits the schema into groups that fit the model, finds the part of the document each group needs, extracts plain key = value lines instead of nested JSON, validates every field against the text, retries the ones that fail, and reassembles the clean nested JSON you asked for. Schemas with thousands of fields come back intact.

On ExtractBench's 369-key SEC filings, every frontier model in the paper (GPT-5, Opus 4.5, Gemini 3 Pro) scores 0%: the schema is too wide to emit in one response. nfield reaches 81-85% on a 27B open model, because it never asks the model to emit the whole schema at once. See the numbers.

Contents

Quickstart

from nfield import nfield

document = """
INVOICE #4471
Vendor: Acme Corporation
Total Due: 1284.50 USD
Date: 2026-01-15
Status: PAID
"""

schema = {
    "type": "object",
    "properties": {
        "vendor": {"type": "string"},
        "invoice_number": {"type": "string"},
        "total": {"type": "number"},
        "currency": {"type": "string"},
        "paid": {"type": "boolean"},
    },
    "required": ["vendor", "total"],
}

result = nfield(document, schema, "groq/llama-3.1-8b-instant")
print(result.data)
# {'vendor': 'Acme Corporation', 'invoice_number': '#4471',
#  'total': 1284.5, 'currency': 'USD', 'paid': True}

The model is a "provider/model-name" string. A Pydantic model or a dataclass works as the schema too, and comes back as the same nested JSON:

from pydantic import BaseModel
from nfield import nfield

class Invoice(BaseModel):
    vendor: str
    total: float
    currency: str
    paid: bool

result = nfield(document, Invoice, "groq/llama-3.1-8b-instant")
# {'vendor': 'Acme Corporation', 'total': 1284.5, 'currency': 'USD', 'paid': True}

Wide schema on a big model? Pass the model's real context_window and max_output_tokens. nfield plans the calls from these numbers, so giving it the true limits (instead of the safe default) lets it use the whole window and split the schema into as few calls as the budget allows.

result = nfield(
    document, schema, "groq/llama-3.3-70b-versatile",
    context_window=131_072,     # the model's real context window
    max_output_tokens=32_768,   # the model's real output limit
)

Install

pip install nfield

nfield's core has one small dependency; providers and features are opt-in extras:

Extra Install Adds
groq pip install "nfield[groq]" Groq provider (groq/...)
openai pip install "nfield[openai]" OpenAI + any OpenAI-compatible endpoint via base_url
google pip install "nfield[google]" Google Gemini provider (google/...)
anthropic pip install "nfield[anthropic]" Anthropic Claude provider (anthropic/...)
openrouter pip install "nfield[openrouter]" OpenRouter (openrouter/...): one key, many model families
cli pip install "nfield[cli]" The nfield command-line tool
export pip install "nfield[export]" pandas / CSV export

OpenAI-compatible presets (deepseek, together, fireworks, mistral, xai, perplexity, cerebras, ollama) install the same way, e.g. pip install "nfield[deepseek]", and route with their own prefix and key. Full list in docs/configuration.md.

Combine them: pip install "nfield[groq,cli,export]".

Set your API key

nfield reads the provider's standard environment variable, so the common path is zero config:

export GROQ_API_KEY="gsk_..."       # or OPENAI_API_KEY, etc.

A .env file works the same way once it is loaded into the environment (nfield reads os.environ; use python-dotenv or your shell to load it). Prefer the env var in production.

For a secret vault or a multi-tenant service, pass the key explicitly instead (it is never logged):

from nfield import NField

engine = NField("groq/llama-3.1-8b-instant", schema, api_key="gsk_...")
result = engine.extract(document)

Point at any OpenAI-compatible endpoint with base_url:

engine = NField("openai/llama-3.1-8b", schema, base_url="http://localhost:11434/v1")

Native prefixes are groq/, openai/, google/ (Gemini), and anthropic/ (Claude). Or use one OPENROUTER_API_KEY to reach many vendors through openrouter/, using OpenRouter's own model slugs:

nfield(document, schema, "openrouter/anthropic/claude-3-haiku")     # Claude
nfield(document, schema, "openrouter/deepseek/deepseek-chat")       # DeepSeek

See docs/configuration.md for every provider and preset.

Benchmarks

Every number is one named model on one date: nfield measures the method, not a leaderboard. Accuracy is the model's job; completeness at scale is nfield's. Full detail, reproduction steps, and manifests in benchmark/README.md.

ExtractBench (Contextual AI, real documents, human-checked gold) on qwen/3.6-27b on Groq:

Domain Coverage Value accuracy Judged Best frontier model (paper)
Research 96.8% 95.5% 95.9% 49.0%
Credit 94.3% 82.3% 86.4% 86.9%
Resumes 87.9% 81.6% 87.3% 24.0%
Sports 99.0% 98.6% 98.8% 18.3%
SEC 10-K/Q (369 keys) 91.2% 81.1% 85.4% 0.0%
Overall 92% 84.2% 87.7% 6.9%

On the 369-key SEC filings every frontier model in the paper scored zero: the schema is too wide to emit in one response, so the JSON truncates and the whole document fails. nfield never asks the model to emit the whole schema at once.

Staying complete as the schema widens (synthetic schemas, one run each): coverage holds at ~100% while the number of model calls tracks the computed minimum, with no call storm.

Fields Coverage Calls (minimum)
2,523 100% 62 (61)
4,000 100% 95 (94)
5,641 ~100% 127 (124)

Why nfield

Built for wide schemas Thousands of fields, not a handful. nfield plans how to split the schema across calls so the model never sees more than it can reliably handle at once.

Grounded, not guessed Every value is validated against the document; fields that fail are retried surgically rather than left wrong. Opt into a hallucination score per value.

Any model Groq, OpenAI, or any OpenAI-compatible endpoint (Together, Fireworks, DeepSeek, vLLM, Ollama, LM Studio) through one base_url. No local weights required.

Typed in, typed out Give it a JSON Schema, a Pydantic model, or a dataclass; get back nested JSON in the same shape. Ships py.typed and passes mypy --strict.

How nfield compares

Different tools live in different lanes. nfield's lane is field count: staying complete when the schema is wide.

Tool Wide schemas (hundreds to thousands of fields) Grounds values to source Works with any remote model
nfield ✅ splits + reassembles ✅ ✅
Instructor single call no ✅
LangExtract chunked entities ✅ ✅
ContextGem aspect / concept ✅ ✅
LangStruct single call, auto-prompt ✅ ✅

nfield's lane is field count: keeping one wide schema complete when it does not fit in a single response. The table covers that one axis; it is not a quality ranking.

Working with files

Real schemas and documents live on disk. The nfield.io helpers load them and persist results as JSON Lines, one record per line:

from nfield import nfield, load_document, load_schema, save_results

schema = load_schema("schemas/clinical_trial.json")   # a JSON Schema with hundreds of fields
document = load_document("records/trial_4471.txt")

result = nfield(
    document, schema, "groq/llama-3.3-70b-versatile",
    context_window=131_072,     # the model's real context window
    max_output_tokens=32_768,   # the model's real output limit
)
save_results([result], "out/trials.jsonl")            # round-trips with load_results

Reusing an engine

For many documents, build the engine once so the schema is parsed and the model calibrated a single time:

from nfield import NField, AsyncNField

engine = NField("groq/llama-3.1-8b-instant", schema)
for doc in documents:
    print(engine(doc).data)

# or run them concurrently
async with AsyncNField("groq/llama-3.1-8b-instant", schema) as engine:
    results = await engine.extract_batch(documents)

Response caching

Run the same extraction twice and the second run is free. Turn on the cache and nfield saves each model response; a repeat request reads it back instead of calling the model. It is off by default and keyed on the exact request, so a hit is the text the model would have returned, never a similar prompt's answer:

from nfield import NField, ExtractionConfig, DiskCache

# In memory for this process, or DiskCache("path") to keep it between runs.
engine = NField("groq/llama-3.1-8b-instant", schema, config=ExtractionConfig(cache=True))

On the command line, add --cache-dir .nfield_cache to extract or batch. See docs/configuration.md for disk and custom backends.

Command line

Install the cli extra, then:

# Offline: field count, type breakdown, and a minimum-call (K_min) estimate. No API calls.
nfield inspect schema.json

# Extract one document (JSON to stdout).
nfield extract invoice.txt --schema schema.json --model groq/llama-3.1-8b-instant

# Opt-ins + a run summary (status, quality, grounding) on stderr; JSON Lines output.
nfield extract invoice.txt -s schema.json -m groq/llama-3.1-8b-instant \
    --ground-values --show-metadata --format jsonl

# Extract a whole directory with one reused engine, streaming JSON Lines.
nfield batch ./docs -s schema.json -m groq/llama-3.1-8b-instant -o out.jsonl

Every ExtractionConfig setting is a flag on extract, grouped into panels in nfield extract --help. --format is json, jsonl, or csv. Both commands exit non-zero when an API/call failure leaves the result incomplete. Full reference: docs/cli.md.

Grounding and provenance

Turn on grounding to label how well the source supports each value, and provenance to get the exact character span each value came from:

from nfield import nfield, ExtractionConfig

result = nfield(
    document, schema, "groq/llama-3.1-8b-instant",
    config=ExtractionConfig(ground_values=True, provenance=True),
)
print(result.metadata.hallucination_rate)   # 0.0
print(result.provenance)
# {'vendor': [22, 38], 'invoice_number': [8, 13], 'total': [50, 56], 'currency': [58, 61]}

Grounding is non-destructive: a weakly-supported value is reported, never silently dropped, because a correct value is often not verbatim (units, derived dates, enum choices).

With provenance on, one call renders the whole extraction as a reviewable page - every located value highlighted in the source document, field names on hover:

from nfield import save_html

save_html(result, document, "review.html")   # self-contained, open in any browser

Know what you spent

Every result carries the run's real token usage, straight from the provider's own counts - no setup. Add your model's prices and nfield does the billing math:

config = ExtractionConfig(pricing=(0.05, 0.08))   # (input, output) USD per million tokens
result = nfield(document, schema, "groq/llama-3.1-8b-instant", config=config)

result.metadata.tokens_prompt       # 712
result.metadata.tokens_completion   # 25
result.metadata.cost                # 3.76e-05

Cache hits make no API call, so a warm rerun reports zero tokens and a cost of 0.0 - the metadata is your receipt that the cache worked.

Configuration

Tune behaviour with ExtractionConfig, passed as config=.... Everything is opt-in with a sane default, and each setting is also a CLI flag. The ones you reach for most:

Setting Default What it does
ground_values False Score each value's support against the source; report hallucination_rate.
provenance False Attach [start, end) source char offsets per value.
reasoning_model False Disable a reasoning model's thinking so it does not eat the output budget.
max_concurrent_calls 4 Leaf calls in flight at once; raise on higher-throughput plans.
fallback_model None Stronger model(s) to escalate still-failing fields to after recovery; a list is tried in order.
pricing None (input, output) USD per million tokens; fills metadata.cost from real usage.
knowledge_fallback False Fill fields absent from the document from the model's own knowledge.
closed_book False Fill the schema from model knowledge with no document at all.

The complete table (25+ settings) is in docs/configuration.md.

Reasoning models

For a reasoning model (Qwen3, DeepSeek-R1, QwQ), set reasoning_model=True so its thinking is turned off per call and does not consume the answer's output budget:

result = nfield(document, schema, "groq/qwen-3.6-27b",
                config=ExtractionConfig(reasoning_model=True))

How it works

A seven-stage pipeline turns one wide request into many small, grounded ones:

calibrate -> analyze schema -> group fields -> retrieve text (BM25)
          -> pack to capacity -> extract (key = value) -> validate + retry -> assemble JSON

Only the extraction and retry stages call the model; everything else is local. Two ideas carry it: the output is field.path = value lines rather than one JSON blob, so a run cannot fail on a truncated brace; and the schema is split so every call stays inside the model's real context and output budget. See docs/concepts/pipeline.md for the full walkthrough.

Documentation

Quickstart Run your first extraction.
Configuration API keys, and every ExtractionConfig setting.
CLI reference inspect, extract, batch, and all flags.
Examples Verified, runnable recipes with real output.
Errors and troubleshooting Every failure, its message, and the fix.
How the pipeline works The seven stages, end to end.
The SFEP format Why key = value beats nested JSON.
API reference Every public name.
Benchmarks ExtractBench, FinTagging, and scaling runs.

Contributing

Issues and pull requests are welcome. Adding a provider is a single registry entry, and the development workflow is uv + ruff + mypy --strict + pytest. See CONTRIBUTING.md.

License

Apache-2.0. See LICENSE.

Metadata

Release files for nfield 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nfield 0.1.1
File Size Uploaded
nfield-0.1.1.tar.gz 3.7 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for nfield 0.1.1
File Interpreter ABI Platform
nfield-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 3.9 MB

Release files / nfield-0.1.1.tar.gz

Download URL nfield-0.1.1.tar.gz
Size 3.7 MB
Tags Source
SHA-256 checksum
How to use checksums
ad74151943fbbca358edbd75eb9e0bc97051e6b673f2e70324ad10946b61dca4
BLAKE2b-256 checksum
How to use checksums
da81b4fb7e42f4401f3bd38fb6bd1c0cc7b7195dd7b7c7ae44a8b404d97ea3cb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 10, 2026.

Transparency log

Release files / nfield-0.1.1-py3-none-any.whl

Download URL nfield-0.1.1-py3-none-any.whl
Size 265.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7e939e5a9928e4d4b736608917c00c69d1c328ff7deaab91d288f05d03a6c9a2
BLAKE2b-256 checksum
How to use checksums
1f9dedae004e746b948825eff6d756ebf9f9d580222dfbd81434022a87cbb5af
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 10, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page