LiTiL Contract Extractor 1.7B

What this model does

LiTiL Contract Extractor answers a specific contract question by returning the relevant language from the agreement. It can capture a date, party, governing-law clause, renewal term, or another supported field, and it uses the stable value NOT_PRESENT when the requested term is not found.

In a contract-intelligence stack, place it after document parsing and retrieval. A classifier or search layer can identify the likely clause, the extractor can capture the exact value or supporting text, and the system can store that answer with its source location. Those structured fields can populate contract records, support comparisons, and provide the facts needed by a playbook or review workflow.

  • Useful for: capturing dates, parties, governing law, renewal terms, and other contract fields
  • Give it: contract text and a question about the term you need
  • It returns: the supporting text or NOT_PRESENT

Model description

Field Value
Hugging Face repository litillabs/litil-contract-extractor-1.7b
Base model Qwen/Qwen3-1.7B
Artifact type PEFT LoRA adapter; the base model is required
Evaluated source revision d8441e71a3dfa66b62d7b3ed1cce12da3e813294
Adaptation PEFT LoRA supervised fine-tuning
Input Contract text, CUAD category, and one extraction question
Output <answer>verbatim span</answer> or <answer>NOT_PRESENT</answer>
Developer LiTiL Labs
Card date September 11, 2026

The matching adapter_model.safetensors is 278,973,888 bytes with SHA-256 cf78d5ff2223fdbcb19202e4b55be352cf86fcc7173c6033f8199cf0e0c58e0c.

Intended use

Use one category-specific question at a time to:

  • extract dates, parties, agreement names, governing law, and other CUAD fields;
  • return a consistent absence value for missing terms;
  • populate structured contract metadata; and
  • send extracted text to a category-specific review step.

For long contracts, retrieve or chunk relevant text before asking the extraction question, then retain the source location beside the extracted span.

Input contract

The user message has three fields:

<context>
CONTRACT TEXT OR RETRIEVED PASSAGE
</context>

Category: Effective Date
Question: What is the effective date of this agreement?

Use a category and question from the CUAD taxonomy. The original preparation code contains the canonical question for all 41 categories. Keep one category per request.

Output contract

Present term:

<answer>Sept 29, 2004</answer>

Absent term:

<answer>NOT_PRESENT</answer>

Parse exactly one <answer> element and preserve the returned span before applying any normalization. Store NOT_PRESENT as a structured null value rather than literal contract text.

Evaluation

The retained test set contains 2,091 contract-question pairs from 51 contracts. The 459 training contracts and 51 test contracts are disjoint.

Metric Qwen3-1.7B base LiTiL adapter Change
Normalized exact match 70.78% 74.46% +3.68 points
Token F1 0.7306 0.7690 +0.0384
Character Jaccard 0.7460 0.7927 +0.0467

Both models used greedy decoding and the same input construction. The adapter's largest saved gains were on document name, agreement date, effective date, parties, and insurance questions.

Training

The adapter was supervised-fine-tuned on public CUAD contracts and annotations.

Setting Value
Training rows 18,819
Training contracts 459
Test rows 2,091
Test contracts 51
Categories 41
Training span-present rows 6,084
Training NOT_PRESENT rows 12,735
Epochs 3
Per-device batch size 2
Gradient accumulation 8
Effective batch size 16
Learning rate 2e-4
Warmup ratio 5%
Weight decay 0.01
Maximum training length 2,048 tokens
LoRA rank / alpha / dropout 64 / 128 / 0.05
Seed 42

The retained source review found public CUAD post-training data and no private post-training source for this run.

Runtime guidance

The adapter is approximately 266 MiB. A 1.7B BF16 base requires roughly 3.4 GB for weights before the adapter, activations, and KV cache; 6–8 GB of accelerator memory is a practical starting point for short or retrieved passages. The full retained comparison ran with vLLM on one NVIDIA L40S at max_model_len=8192, greedy decoding, and 200 generated tokens. The training distribution used a 2,048-token envelope, so category-focused excerpts are the best-aligned input.

Limitations

  • Results cover the 41 CUAD question categories and should not be generalized to arbitrary extraction schemas without testing.
  • Long-contract quality depends on retrieval or chunking because the training examples were length-limited.
  • Aggregate scores include both span extraction and NOT_PRESENT cases; track those two behaviors separately in an application evaluation.

Use the model

Installation

The adapter records PEFT 0.19.1. Qwen3 support requires a current Transformers release.

python -m pip install \
  "torch>=2.1" \
  "transformers>=4.51" \
  "peft>=0.19.1" \
  "accelerate>=1.0" \
  "safetensors>=0.5"

Loading and inference

import re

import torch
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer

BASE_ID = "Qwen/Qwen3-1.7B"
ADAPTER_ID = "litillabs/litil-contract-extractor-1.7b"

SYSTEM_PROMPT = (
    "You are a legal contract analysis assistant. "
    "Given a contract excerpt, extract the specific clause or information requested. "
    "If the information is present, output it verbatim inside <answer>...</answer> tags. "
    "If the information is not present in the contract, output <answer>NOT_PRESENT</answer>. "
    "Be precise and extract only the relevant text."
)


def build_messages(context: str, category: str, question: str):
    return [
        {"role": "system", "content": SYSTEM_PROMPT},
        {
            "role": "user",
            "content": (
                f"<context>\n{context.strip()}\n</context>\n\n"
                f"Category: {category}\n"
                f"Question: {question}"
            ),
        },
    ]


def parse_answer(text: str):
    text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL | re.IGNORECASE).strip()
    match = re.search(r"<answer>(.*?)</answer>", text, flags=re.DOTALL | re.IGNORECASE)
    if not match:
        raise ValueError(f"Response did not contain one <answer> block: {text!r}")
    value = match.group(1).strip()
    return None if value.upper() == "NOT_PRESENT" else value


tokenizer = AutoTokenizer.from_pretrained(
    ADAPTER_ID,
)
base = AutoModelForCausalLM.from_pretrained(
    BASE_ID,
    dtype=torch.bfloat16,
    device_map="auto",
    trust_remote_code=True,
)
model = PeftModel.from_pretrained(
    base,
    ADAPTER_ID,
).eval()

messages = build_messages(
    context="THE EFFECTIVE DATE OF THIS RESELLER AGREEMENT SHALL BE: Sept 29, 2004",
    category="Effective Date",
    question="What is the effective date of this agreement?",
)
input_ids = tokenizer.apply_chat_template(
    messages,
    tokenize=True,
    add_generation_prompt=True,
    return_tensors="pt",
).to(model.device)

with torch.inference_mode():
    generated = model.generate(
        input_ids=input_ids,
        do_sample=False,
        max_new_tokens=200,
        eos_token_id=tokenizer.eos_token_id,
        pad_token_id=tokenizer.pad_token_id,
    )

text = tokenizer.decode(generated[0, input_ids.shape[1]:], skip_special_tokens=True)
print(parse_answer(text))

Citation

@inproceedings{hendrycks2021cuad,
  title     = {CUAD: An Expert-Annotated NLP Dataset for Legal Contract Review},
  author    = {Hendrycks, Dan and Burns, Collin and Chen, Anya and Ball, Spencer},
  booktitle = {NeurIPS Datasets and Benchmarks},
  year      = {2021}
}
Downloads last month
17
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for litillabs/litil-contract-extractor-1.7b

Finetuned
Qwen/Qwen3-1.7B
Adapter
(672)
this model

Dataset used to train litillabs/litil-contract-extractor-1.7b

Collection including litillabs/litil-contract-extractor-1.7b

Evaluation results

  • Normalized exact match on CUAD contract-disjoint test split
    self-reported
    0.745
  • Token F1 on CUAD contract-disjoint test split
    self-reported
    0.769
  • Character Jaccard on CUAD contract-disjoint test split
    self-reported
    0.793