Skip to content

Budgets, pricing, and cache

Hard budgets

Pass budget to read() or execute() to set a hard ceiling. Before each provider attempt, docgate compares the provider's one-page estimate to the remaining budget. It does not start an attempt that would exceed the ceiling.

from decimal import Decimal

result = docgate.read(
    "bundle.pdf",
    providers=[vision],
    budget=Decimal("0.10"),
)
assert result.receipt.spent <= Decimal("0.10")

The default on_budget_exceeded="stop" returns partial results. Selected pages that were not attempted are recorded as BUDGET_EXHAUSTED. Pass on_budget_exceeded="raise" to execute() when the application should fail instead; it raises RuntimeError after the budget condition is reached.

Budgeting is based on the provider's quoted estimate, not a promise of a provider's eventual invoice. Cache hits cost zero to the docgate receipt.

Record pricing provenance

Provider pricing is supplied by your application because commercial pricing can change. PricingProfile preserves the per-page value, the verification date, and an optional source in each accepted provider CostQuote.

from datetime import date
from decimal import Decimal
import docgate

pricing = docgate.PricingProfile(
    per_page=Decimal("0.004"),
    verified_on=date(2026, 8, 30),
    source="https://provider.example/pricing",
)
ocr = docgate.MistralOcrProvider(api_key="...", pricing=pricing)

Use receipt.spent (also exposed as estimated_spend) and receipt.quotes for the run-level record. A quote is added for a successful provider response, including one that the quality gate later rejects before fallback.

SQLite cache

SQLiteCache persists successful non-empty transcriptions locally.

import docgate

cache = docgate.SQLiteCache(".docgate-cache.db")
result = docgate.read("scan.pdf", providers=[vision], cache=cache)

The execution key combines the SHA-256 hash of the exact rendered PNG bytes with the provider's non-secret cache_identity. It does not use a document path or filename. Empty, malformed, and failed responses are not cached. This is why a cache hit is safe to treat as a completed page but a blank provider response should be handled by the provider ladder instead.