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+.
Install from Git (recommended for private use)
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.23#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(llm.chat("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
- Igor Nikolaienko
- Harsh Bansal
Usage
from fintom8 import LLM, facturx_to_bt_json, zugferd_to_bt_json
llm = LLM()
resp = llm.chat("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 = llm.structured(
"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 = llm.generate_and_validate(name="invoice", source="invoice.pdf")
# {"correct": true, "name": "invoice", "format": "json", "artifact": {…}, …}
en16931 = llm.generate_and_validate(
name="en16931", source="invoice.pdf", invoice_format="ubl"
)
# en16931["artifact"] is the last UBL XML string; requires fintom8[schematron]
chem = llm.generate_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 = llm.convert("invoice.pdf", invoice_format="ubl")
zugferd_xml = llm.convert("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 = zugferd_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 = facturx_to_bt_json("invoice.xml")
# fx["BT-1"] == {"description": "Invoice Number", "value": "INV-1"}
# process() is an alias of structured()
data = llm.process(
"invoice.pdf",
structuredOutput=invoice_rf,
dateFormat="DD.MM.YYYY",
instructions="Extract invoice vendor and total.",
)
for chunk in llm.stream([{"role": "user", "content": "Write a haiku"}]):
print(chunk, end="", flush=True)
resp = llm.extract("invoice.pdf", response_format=invoice_rf)
Async twins: achat, astream, aextract, astructured, aprocess, aconvert, azugferd_to_bt_json, afacturx_to_bt_json, agenerate_and_validate, acorrect_facturx_from_pdf, acompare, adetect_pii, aanonymize.
Optional helpers: detect_format, fields_to_schema, compile_fields, prepare_response_format, apply_cleanse, json_schema_response_format, structured_output, enforce_strict, inline_refs, compare, acompare.
Compare two sources
LLM-based dual-source comparison with built-in profiles or a custom schema:
from fintom8 import LLM, compare
llm = LLM()
# PDF/image/document vs invoice XML (payment-critical mismatches)
report = llm.compare("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 = llm.compare(
"mapping.html",
"PEPPOL-EN16931-UBL.sch",
profile="mapping_reference",
max_findings=5,
model="gemini/gemini-3.1-pro-preview",
)
# Two UBL XML invoices — syntax/structural diffs only (not value-only changes)
xml_report = llm.compare(
"invoice_original.xml",
"invoice_corrected.xml",
profile="xml-comparison",
max_findings=10,
)
# Custom comparison
custom = llm.compare(
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 = llm.structured(
"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 validate, erp_check
report = validate("invoice", payload)
report = validate("chemical_composition", payload)
report = validate("recipient_statement", payload)
# EN16931 Schematron on UBL or CII XML (format is detected; requires fintom8[schematron])
en16931 = validate("en16931", {"xml": ubl_or_cii_xml})
# or: validate("en16931", {"xml_path": "invoice.xml"})
# Factur-X EN 16931 XSD on CII XML (ZUGFeRD Comfort successor; requires fintom8[xsd])
facturx = validate("facturx_en16931", {"xml": cii_xml})
# ERP check is named + injected. qc55 is SAP vs chemical coils only.
qc55 = erp_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. generate_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 create_pdfa3, extract_xml, get_facturx_xml_from_pdf, mask_pdf
pdfa3_bytes = create_pdfa3("invoice.pdf", "invoice.xml")
xml = extract_xml(pdfa3_bytes) # or extract_xml("invoice_zugferd.pdf")
# factur-x-style extract: (filename, xml_bytes) or (False, False)
name, xml_bytes = get_facturx_xml_from_pdf("invoice_zugferd.pdf")
masked = mask_pdf(
"invoice.pdf",
{"Acme GmbH": "Demo Corp", "DE123456789": "DE000000000"},
)
Path("masked.pdf").write_bytes(masked.pdf_bytes)
mask_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 detect_pii plus mask_pdf, or anonymize_pdf for both steps:
from pathlib import Path
from fintom8 import LLM, detect_pii, mask_pdf, anonymize_pdf
llm = LLM()
# LLM call only — same mapping the anonymizer endpoint feeds into mask_pdf
items = llm.detect_pii("invoice.pdf")
mapping = {item.original: item.replacement for item in items}
masked = mask_pdf("invoice.pdf", mapping)
# Or one shot: detect + mask
result = llm.anonymize("invoice.pdf")
Path("anonymized.pdf").write_bytes(result.pdf_bytes)
# result.items, result.mapping, result.applied_counts
See examples/anonymize_pdf.py. aanonymize / adetect_pii are the async twins.
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 validate, erp_check
chem = validate("chemical_composition", extracted)
qc55 = erp_check("qc55", extracted) # or erp_check("qc55", extracted, reference=live_rows)
Optional aliases: validate_invoice, validate_recipient_statement, validate_en16931, validate_facturx_en16931, erp_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 with invoice_format="zugferd" (or cii / factur-x), the validate step runs Schematron and the Factur-X EN 16931 XSD together. The corrector receives both error lists and the XML once. UBL stays Schematron-only.
from fintom8 import LLM
llm = LLM()
report = llm.generate_and_validate(name="invoice", source="invoice.pdf", include_debug=True)
report = llm.generate_and_validate(
name="en16931", source="invoice.pdf", invoice_format="ubl", include_debug=True
)
# report["artifact"], report["_debug"]["attempts"]
report = llm.generate_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)
For an existing Factur-X / ZUGFeRD PDF: extract the embedded XML, then run the same EN16931 cleanse / Schematron (+ XSD) / LLM correction loop. Attempt 1 uses the extracted XML (no generate-from-scratch). Corrected XML is report["artifact"].
from fintom8 import LLM
llm = LLM()
report = llm.correct_facturx_from_pdf("invoice_facturx.pdf", include_debug=True)
corrected_xml = report["artifact"]
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 generate_and_validate(name="en16931", source=…) instead.
from fintom8 import LLM, convert_to_xml
llm = LLM()
ubl = llm.convert("invoice.pdf", invoice_format="ubl")
cii = llm.convert("invoice.json", invoice_format="zugferd") # aliases: cii, factur-x
# or: convert_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)
-
Install dev extras and run tests:
cd fintom8 pip install -e ".[dev]" pytest python -c "from fintom8 import LLM"
-
Build:
python -m build
-
Upload to TestPyPI first, then PyPI:
python -m twine upload --repository testpypi dist/* python -m twine upload dist/*
-
Tag for CI Trusted Publishing (OIDC). Create the PyPI project once and add a GitHub environment
pypiwith Trusted Publisher pointing at.github/workflows/publish-fintom8.yml. Then:git tag fintom8-v0.1.23 git push origin fintom8-v0.1.23
Metadata
Release files for fintom8 0.1.23
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| fintom8-0.1.23.tar.gz | 282.2 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fintom8-0.1.23-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 586.9 kB
Release files / fintom8-0.1.23.tar.gz
| Download URL | fintom8-0.1.23.tar.gz |
|---|---|
| Size | 282.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
209c8c1adbe2a8f7306fb234b0cad9a3e50b7f8b4289d2be1fd640942a29b730
|
|
BLAKE2b-256 checksum How to use checksums |
e6bc951dbe6a23a27f3c27aa05eb01d560c3dc4a5d6ad67ac97bb06a39eb2770
|
| 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.23-py3-none-any.whl
| Download URL | fintom8-0.1.23-py3-none-any.whl |
|---|---|
| Size | 304.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2b811fcce541c4dbb7853d7dfb082dc7f2d501ffa0cb780eeb69e06733d575b1
|
|
BLAKE2b-256 checksum How to use checksums |
a0b9aacabc4f55229489b467f8e85e650dcf7d658b9eca22fea0e6fd9e5926f2
|
| 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.23
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.