Skip to main content

fintom8

LiteLLM connector for Gemini / Vertex AI / OpenAI / Azure. Chat, stream, and document extract. Students install with pip and call a few methods — keys stay in .env.

Requires Python 3.14+.

GitHub Packages Python upload is currently unreliable (SSL issues). Install directly from this repo instead:

# HTTPS (use a PAT with repo read access if the repo is private)
pip install "git+https://github.com/NikolaienkoIgor/f8_templates.git#subdirectory=fintom8"

# Pin a tag / commit
pip install "git+https://github.com/NikolaienkoIgor/f8_templates.git@fintom8-v0.1.46#subdirectory=fintom8"

# SSH (no token in the URL if your SSH key is set up)
pip install "git+ssh://git@github.com/NikolaienkoIgor/f8_templates.git#subdirectory=fintom8"

Private HTTPS with an explicit token:

pip install "git+https://<GITHUB_USERNAME>:<GITHUB_PAT>@github.com/NikolaienkoIgor/f8_templates.git#subdirectory=fintom8"

Other install options

# Public PyPI (when published)
pip install fintom8

# Local editable install from this checkout
pip install -e ./fintom8
# or: pip install -e "./fintom8[dev]"
from fintom8 import LLM

llm = LLM()  # reads .env / environment
print((await llm.achat("Summarize this invoice")).text)

Configuration

Resolution order: constructor kwargs / LLMConfig > environment > defaults.

Copy .env.example to .env in your project (never commit it).

Param Env Default When needed
model LLM_MODEL gemini/gemini-3.5-flash always
temperature LLM_TEMPERATURE unset (Gemini 3+), 1.0 (older Gemini/Vertex), else 0.0 optional; omitted for Gemini 3+ (deprecated by Google)
num_retries — 3 optional
api_key GEMINI_API_KEY / OPENAI_API_KEY / AZURE_API_KEY (from model prefix) unset Gemini / OpenAI / Azure (azure/ → required)
api_base AZURE_API_BASE / OPENAI_API_BASE unset Azure (azure/ → required)
api_version AZURE_API_VERSION unset Azure (azure/ → required)
vertex_project VERTEXAI_PROJECT unset Vertex
vertex_location VERTEXAI_LOCATION eu Vertex

For azure/<deployment>, missing api_key, api_base, or api_version raises Fintom8Error (set via constructor or AZURE_* env vars).

from fintom8 import LLM, LLMConfig

llm = LLM()  # env defaults
llm = LLM(model="gpt-4o", api_key="sk-...", temperature=0)
llm = LLM(LLMConfig(
    model="azure/my-deploy",
    api_key="...",
    api_base="https://....openai.azure.com",
    api_version="2024-10-21",
))

Switch provider

LLM_MODEL Env
gemini/gemini-3.5-flash GEMINI_API_KEY
vertex_ai/gemini-3.5-flash VERTEXAI_PROJECT + VERTEXAI_LOCATION + ADC (gcloud auth application-default login)
gpt-4o OPENAI_API_KEY
azure/<deployment> AZURE_API_KEY + AZURE_API_BASE + AZURE_API_VERSION

Contributors

Usage

from fintom8 import LLM, afacturx_to_bt_json, azugferd_to_bt_json

llm = LLM()

resp = await llm.achat("Hello")
print(resp.text, resp.usage)

# Structured output — file only; detect format → LiteLLM SO → cleanse dates
invoice_rf = {
    "type": "json_schema",
    "json_schema": {
        "name": "Invoice",
        "strict": True,
        "schema": {
            "type": "object",
            "properties": {
                "total": {"type": "number"},
                "vendor": {"type": "string"},
                "invoiceDate": {"type": ["date", "null"]},
            },
            "required": ["total", "vendor", "invoiceDate"],
            "additionalProperties": False,
        },
    },
}
data = await llm.astructured(
    "invoice.pdf",  # also: .png/.jpg, .txt/.csv/.xml/.xlsx, or bytes
    structuredOutput=invoice_rf,
    dateFormat="DD.MM.YYYY",
)
# {"total": 42.5, "vendor": "Acme", "invoiceDate": "08.08.2026"}

# Invoice / EN16931 / chem ping-pong (keyword args)
report = await llm.agenerate_and_validate(name="invoice", source="invoice.pdf")
# {"correct": true, "name": "invoice", "format": "json", "artifact": {…}, …}

en16931 = await llm.agenerate_and_validate(
    name="en16931", source="invoice.pdf", invoice_format="ubl"
)
# en16931["artifact"] is the last UBL XML string; requires fintom8[schematron]

chem = await llm.agenerate_and_validate(name="chemical_composition", source="cert.pdf")
# Prefer a path for source; pass filePath= only when source is raw bytes

# Document → UBL / ZUGFeRD XML (LLM generate only; no Schematron loop)
ubl_xml = await llm.aconvert("invoice.pdf", invoice_format="ubl")
zugferd_xml = await llm.aconvert("invoice.csv", invoice_format="zugferd")
# also: .json / .xml / images / .xlsx — or bytes with mime= / filePath=

# ZUGFeRD / Factur-X CII XML → EN 16931 BT JSON (same result as POST /zugferd2json/)
bt = await azugferd_to_bt_json("invoice.xml")  # or XML text / bytes
# bt["BT-1"] == {"description": "Invoice number", "value": "INV-1"}

# Factur-X CII or UBL → factur-x field ids (no XSD check)
fx = await afacturx_to_bt_json("invoice.xml")
# fx["BT-1"] == {"description": "Invoice Number", "value": "INV-1"}

# process() is an alias of structured()
data = await llm.aprocess(
    "invoice.pdf",
    structuredOutput=invoice_rf,
    dateFormat="DD.MM.YYYY",
    instructions="Extract invoice vendor and total.",
)

async for chunk in llm.astream([{"role": "user", "content": "Write a haiku"}]):
    print(chunk, end="", flush=True)

resp = await llm.aextract("invoice.pdf", response_format=invoice_rf)

Async-only API: use achat, astream, aextract, astructured, aprocess, aconvert, azugferd_to_bt_json, afacturx_to_bt_json, agenerate_and_validate, acorrect_and_validate, acorrect_facturx_from_pdf, acompare, adetect_pii, aanonymize, avalidate, aerp_check, acreate_pdfa3, aextract_xml, amask_pdf, aget_facturx_xml_from_pdf.

Optional helpers: detect_format, fields_to_schema, compile_fields, prepare_response_format, apply_cleanse, json_schema_response_format, structured_output, enforce_strict, inline_refs, acompare.

Compare two sources

LLM-based dual-source comparison with built-in profiles or a custom schema:

from fintom8 import LLM, acompare

llm = LLM()

# PDF/image/document vs invoice XML (payment-critical mismatches)
report = await llm.acompare("invoice.pdf", "invoice.xml", profile="document_xml")
# {"matches": true, "total_mismatches": 0, "mismatches": []}

# Mapping document vs reference rules text (Peppol gap analysis step 1)
findings = await llm.acompare(
    "mapping.html",
    "PEPPOL-EN16931-UBL.sch",
    profile="mapping_reference",
    max_findings=5,
    model="gemini/gemini-3.1-pro-preview",
)

# Two invoice XMLs — structural diffs + calculation fixes (e.g. BR-CO-15)
xml_report = await llm.acompare(
    "invoice_original.xml",
    "invoice_corrected.xml",
    profile="xml-comparison",
    max_findings=10,
)

# Custom comparison
custom = await llm.acompare(
    doc_a,
    doc_b,
    profile="custom",
    instructions="Compare these two contracts for clause differences.",
    structuredOutput={"differences": {"type": "array", "items": {"type": "string"}}},
)

Register additional profiles with fintom8.comparison.register_compare_profile(). See examples/compare_document_xml.py and examples/compare_xml.py.

Bundled templates

from fintom8.templates import invoice
# or: from fintom8 import templates; templates.invoice
# or: from fintom8 import use_template; use_template("invoice")

data = await llm.astructured(
    "invoice.pdf",
    structuredOutput=invoice["structuredOutput"],
    systemPrompt=invoice["systemPrompt"],
    dateFormat=invoice.get("dateFormat", "YYYY-MM-DD"),
)

list_templates() lists packaged names (invoice, chemical_composition, recipient_statement). Pass a path or dict to use_template for custom templates.

Validation

One engine, one envelope. Document types differ only by the pack registered for that name. ERP check is a second step on the same envelope — not a different API.

from fintom8 import avalidate, aerp_check

report = await avalidate("invoice", payload)
report = await avalidate("chemical_composition", payload)
report = await avalidate("recipient_statement", payload)

# EN16931 Schematron on UBL or CII XML (format is detected; requires fintom8[schematron])
en16931 = await avalidate("en16931", {"xml": ubl_or_cii_xml})
# or: await avalidate("en16931", {"xml_path": "invoice.xml"})

# Factur-X EN 16931 XSD on CII XML (ZUGFeRD Comfort successor; requires fintom8[xsd])
facturx = await avalidate("facturx_en16931", {"xml": cii_xml})
# UBL 2.1 XSD on Invoice or CreditNote XML (same optional extra)
ubl = await avalidate("ubl_2.1", {"xml": ubl_xml})

# ERP check is named + injected. qc55 is SAP vs chemical coils only.
qc55 = await aerp_check("qc55", chem_payload, reference=qc55_rows)  # bundled qc55.csv if omitted

Every call returns:

{
  "correct": false,
  "name": "invoice",
  "format": "json",
  "errors": [{"path": "lineItems[0].totalPriceWithTax", "message": "...", "code": "FORMULA"}],
  "warnings": [],
  "artifact": {},
  "units": null,
  "_debug": null
}

artifact is the document (invoice JSON, chemical extraction, recipient JSON, or EN16931 XML string). format is json, ubl, or cii. Chemical and QC55 put per-coil reports in units (including a single coil). Invoice rewritten totals live in artifact (the caller's payload is not mutated). QC55 match rows live in units[coil_id].artifact. EN16931 format is ubl or cii; Schematron rule ids (e.g. BR-CO-15) are in errors[].code and XPath in errors[].path. _debug is {attempts, exhausted, raw} when include_debug=True. agenerate_and_validate(..., include_debug=True) also adds _debug.history: one {attempt, correct, errors, warnings} per ping-pong iteration, so a later pass still shows why earlier attempts failed. Top-level errors is only the last attempt.

Install Schematron support with:

pip install "fintom8[schematron]"

PDF/A-3 (embed invoice XML into a PDF) and PDF text masking are available via:

pip install "fintom8[pdf]"
from pathlib import Path
from fintom8 import acreate_pdfa3, aextract_xml, aget_facturx_xml_from_pdf, amask_pdf

pdfa3_bytes = await acreate_pdfa3("invoice.pdf", "invoice.xml")
# Re-embedding replaces any prior *.xml attachments (non-XML attachments kept).
xml = await aextract_xml(pdfa3_bytes)  # or await aextract_xml("invoice_zugferd.pdf")
# factur-x-style extract: (filename, xml_bytes) or (False, False)
name, xml_bytes = await aget_facturx_xml_from_pdf("invoice_zugferd.pdf")

masked = await amask_pdf(
    "invoice.pdf",
    {"Acme GmbH": "Demo Corp", "DE123456789": "DE000000000"},
)
Path("masked.pdf").write_bytes(masked.pdf_bytes)

amask_pdf finds each original string on the page, paints a cover box with the replacement, and by default raster-flattens so the original text layer is destroyed. Hit counts are in masked.applied_counts.

To detect PII with the LLM and then mask (anonymizer endpoint), use adetect_pii plus amask_pdf, or aanonymize_pdf for both steps:

from pathlib import Path
from fintom8 import LLM, adetect_pii, amask_pdf, aanonymize_pdf

llm = LLM()

# LLM call only — same mapping the anonymizer endpoint feeds into amask_pdf
items = await llm.adetect_pii("invoice.pdf")
mapping = {item.original: item.replacement for item in items}
masked = await amask_pdf("invoice.pdf", mapping)

# Or one shot: detect + mask
result = await llm.aanonymize("invoice.pdf")
Path("anonymized.pdf").write_bytes(result.pdf_bytes)
# result.items, result.mapping, result.applied_counts

See examples/anonymize_pdf.py. Use aanonymize / adetect_pii (async-only).

Bundled EN16931 XSLT files are licensed under EUPL 1.2 (CEN). Default pip install fintom8 does not require Saxon or pikepdf.

Chemical composition document validation does not call QC55; sequence them explicitly:

from fintom8 import avalidate, aerp_check

chem = await avalidate("chemical_composition", extracted)
qc55 = await aerp_check("qc55", extracted)  # or await aerp_check("qc55", extracted, reference=live_rows)

Optional aliases: avalidate_invoice, avalidate_recipient_statement, avalidate_en16931, avalidate_facturx_en16931, avalidate_ubl, aerp_check_qc55 — same envelope as the named calls above.

Generate + validate (ping-pong)

One standard wrapper. name selects SO (JSON schema) or NON-SO (XML); source is the document path or bytes. Same Validation Wrapper loop: validate → on error, correction prompt + regenerate → until correct or max attempts. For en16931, the validate step runs Schematron and the matching XSD together: Factur-X EN 16931 when invoice_format is "zugferd" (or cii / factur-x), and UBL 2.1 when it is "ubl". The corrector receives both error lists and the XML once.

from fintom8 import LLM

llm = LLM()
report = await llm.agenerate_and_validate(name="invoice", source="invoice.pdf", include_debug=True)

report = await llm.agenerate_and_validate(
    name="en16931", source="invoice.pdf", invoice_format="ubl", include_debug=True
)
# report["artifact"], report["_debug"]["attempts"]

report = await llm.agenerate_and_validate(name="chemical_composition", source="cert.pdf")

Packs stay pure (no LLM/retry inside validation/). Pass filePath= / mime= only when source is raw bytes.

Correct Factur-X PDF (extract → validate → fix → PDF/A-3)

For an existing Factur-X / ZUGFeRD PDF: extract the embedded XML, run the EN16931 cleanse / Schematron (+ XSD) / LLM correction loop on the XML only (PDF is not sent to the LLM), then acreate_pdfa3 (any prior *.xml attachments are removed and replaced). Attempt 1 uses the extracted XML (no generate-from-scratch).

from pathlib import Path
from fintom8 import LLM

llm = LLM()
report = await llm.acorrect_facturx_from_pdf("invoice_facturx.pdf", include_debug=True)
corrected_xml = report["artifact"]       # str
Path("corrected.pdf").write_bytes(report["pdfa3"])  # PDF/A-3 bytes

See examples/correct_facturx_from_pdf.py.

Convert document → UBL / ZUGFeRD XML (generate only)

Same idea as the platform invoice-agent generate step without the Schematron loop. For generate + validate + fix, use agenerate_and_validate(name="en16931", source=…) instead.

from fintom8 import LLM, aconvert_to_xml

llm = LLM()
ubl = await llm.aconvert("invoice.pdf", invoice_format="ubl")
cii = await llm.aconvert("invoice.json", invoice_format="zugferd")  # aliases: cii, factur-x
# or: await aconvert_to_xml(llm, "invoice.pdf", invoice_format="ubl")

See examples/convert_to_xml.py, examples/validate_en16931.py.

See examples/invoice_extraction_fintom8.py, examples/generate_and_validate.py, examples/validate_invoice.py, examples/validate_chemical_composition.py, examples/validate_recipient_statement.py, examples/validate_en16931.py, examples/erp_check_qc55.py.

Also exported: Invoice, LineItem, ValidationReport, FieldError, validation_payload_from_llm. Packs are pure (dict in, report out) — no LLM, HTTP, or retry loops.

Failures raise Fintom8Error.

If you see an authentication error (for example missing GEMINI_API_KEY, OPENAI_API_KEY, or Vertex setup), that means package import and retries are working; configure credentials for the selected LLM_MODEL.

See examples/chat.py and examples/extract.py.

Publish (maintainers)

  1. Install dev extras and run tests:

    cd fintom8
    pip install -e ".[dev]"
    pytest
    python -c "from fintom8 import LLM"
    
  2. Build:

    python -m build
    
  3. Upload to TestPyPI first, then PyPI:

    python -m twine upload --repository testpypi dist/*
    python -m twine upload dist/*
    
  4. Tag for CI Trusted Publishing (OIDC). Create the PyPI project once and add a GitHub environment pypi with Trusted Publisher pointing at .github/workflows/publish-fintom8.yml. Then:

    git tag fintom8-v0.1.46
    git push origin fintom8-v0.1.46
    

Metadata

Release files for fintom8 0.1.46

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

Source distribution (sdist)

Source distribution for fintom8 0.1.46
File Size Uploaded Provenance Actions
fintom8-0.1.46.tar.gz 466.0 kB Without provenance Details for fintom8-0.1.46.tar.gz

Built distribution (wheel)

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

Total release size: 977.8 kB

Release files / fintom8-0.1.46.tar.gz

Download URL fintom8-0.1.46.tar.gz
Size 466.0 kB
Tags Source
SHA-256 checksum
How to use checksums
c5b95881b7552abbb2537f61be6032f0fdcbfb72fbb22fcec87e00d3b76d1535
BLAKE2b-256 checksum
How to use checksums
8988b1e44f1fdecc3c8304c4680ce3ed108df3c200ff82fa79f397cda569ea6b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / fintom8-0.1.46-py3-none-any.whl

Download URL fintom8-0.1.46-py3-none-any.whl
Size 511.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
47233a5d67c30928f8f9bec1a9fed8b95345257286e32d84065f704bc5019ad4
BLAKE2b-256 checksum
How to use checksums
d81070bd5598b05de6d0ff4836c99a356d2e165307e2c313f84f7fa5351ab842
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Security for fintom8 0.1.46

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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.46 This release

2 release files No provenance

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