hajer — the Python SDK
At the point where a model output crosses into a side effect — an HTTP response, an email, a database write — hand Hajer the request, the output and the evidence you chose. Hajer applies a named, versioned verifier and returns an assessment. Your application decides what to do with it.
This package is that call. It does not run your application, does not sit in your provider path, and does not decide anything on your behalf. It can also record the model calls your application makes, so an assessment is read beside the calls that produced the output.
Install
pip install hajer # httpx + pydantic, nothing else
pip install "hajer[openai]" # with the OpenAI SDK alongside
pip install "hajer[anthropic]" # with the Anthropic SDK alongside
pip install "hajer[otel]" # optional coexistence with an existing OpenTelemetry setup
pip install "hajer[ci]" # the pytest plugin that runs Hajer suites in CI
The openai and anthropic extras are a convenience: the SDK never imports either library.
wrap(client) instruments the object you hand it, by attribute.
Python ≥ 3.11, httpx>=0.28.1, pydantic>=2.12. The pydantic floor is deliberately low: this package
installs into your environment, and a floor above your pin would make it uninstallable rather than
make you upgrade.
Configure
Three environment variables, read once when the client is constructed:
| Variable | Required | Meaning |
|---|---|---|
HAJER_API_KEY |
yes | Your team API key. |
HAJER_TEAM_ID |
yes | The team every request is scoped to. |
HAJER_BASE_URL |
no | The service. Defaults to https://api.hajer.ai; set it only for a local or self-hosted platform. |
export HAJER_API_KEY=...
export HAJER_TEAM_ID=...
Getting a key. In the Hajer app, Settings → API keys → Create key. The key is shown once, together
with the team id and the base URL as a .env block you can copy as is; the team id stays beside the
page's title afterwards. If you do not have access to a team yet, contact the Hajer team.
Inert without a key. Without HAJER_API_KEY and HAJER_TEAM_ID — or with HAJER_DISABLED=1 — the
client is inert: verify returns Assessment(status="unavailable", reason="DISABLED") immediately with
no socket, observe returns a receipt in state disabled, and nothing raises. The integration can land
in a repository whose test suite has no Hajer credentials and pass unchanged. A missing key is not a
configuration error.
Set HAJER_ENVIRONMENT (production, staging, dev) in every environment you want Hajer to learn
from. Every other setting has a default; the full table is in the
reference.
Quickstart
import hajer
from openai import OpenAI
hajer_client = hajer.Hajer() # reads HAJER_API_KEY and HAJER_TEAM_ID from the environment
openai_client = hajer.wrap(OpenAI()) # model calls are recorded and attached to the next verify
def handle_ticket(ticket, order):
reply = openai_client.chat.completions.create(
model="gpt-5", messages=[{"role": "user", "content": ticket.question}]
).choices[0].message.content
assessment = hajer_client.verify(
"refund-policy@1", # the verifier, pinned: there is no "latest"
{"ticketId": ticket.id, "question": ticket.question}, # the request
reply, # the output, as it will be sent
{"orderState": order.state, "approvedPolicy": order.policy}, # the evidence you chose
)
if assessment.status == "violated":
return hold_for_review(reply, assessment.findings)
return send(reply)
Three things about those lines, because they are the whole design:
- Explicit evidence fields.
{"orderState": order.state}, neverorder.to_dict(). A verifier's evidence contract names fields; an assessment can only be about what it was given. - The final payload, before the effect.
verifyis called on the string that is about to be sent, after every transformation, not on the raw model output. - The application decides.
verifyanswers; yourifacts. Nothing in this SDK holds, retries or sends on your behalf.
assessment.status is one of satisfied, violated, insufficient_evidence or unavailable.
To try one verify end to end against a verifier that already exists, see
Your first row.
Concepts
verify and observe. verify is synchronous with your request path: it returns an Assessment
within deadline_ms (default 1500 ms) and never raises — a transport failure, a timeout or a 5xx is
an unavailable assessment whose reason says which. There is no automatic retry, because a late answer
cannot justify an effect you already performed. observe takes the same arguments, puts the submission
on a bounded in-memory queue and returns a receipt at once; a background worker flushes it, and the
receipt moves through queued, accepted and complete. Use observe where nothing waits on the
answer, including semantic checks too slow to run inline. AsyncHajer is the same client for asyncio.
wrap(client). Instruments an OpenAI, Anthropic, google-genai or LangChain chat model client — or
the litellm module — in place and returns the same object. Each call it makes is recorded (provider,
model, settings, tool calls and results, usage, timing, and message content, redacted client-side) and
attached as wrappedCalls to the next verify or observe in the same task. hajer.instrument() does
the same for clients constructed later, when you cannot reach the construction site.
scope(). Frameworks often make provider calls in child asyncio tasks, whose context never reaches
the parent's verify. A scope collects every call made inside it, whatever task made it:
with hajer.scope(workflow="support-answer"):
reply = await agent.ainvoke(question)
assessment = hajer_client.verify(...)
Attach mode. For an application nobody has instrumented yet: every provider call made outside a
scope() becomes one observe observation of its own, with no verifier — which is how you find out what
the workflows are before writing an obligation about any of them. It is opt-in:
PYTHONPATH="$(python -m hajer attach-path)" HAJER_ATTACH=1 python -m your_app # no code change
import hajer.autoattach # one line in the entry point; reads HAJER_ATTACH, so it is safe to keep
Client-side redaction is on by default. Card numbers, IBANs, national ids, credentials, email
addresses and similar shapes are replaced with [redacted:<CATEGORY>] before anything leaves the
process. hajer.build_policy(...) adjusts it per client or per call.
Command line
python -m hajer doctor # every HAJER_* setting in force, where it came from, and whether the service answers
python -m hajer tail --follow # one line per recorded observation as it lands
python -m hajer proxy --upstream https://api.anthropic.com --listen 127.0.0.1:8091 # record at the wire
python -m hajer attach-path # the directory to put on PYTHONPATH for attach mode
doctor is the first command to run when nothing is arriving. It never prints your key.
Supported libraries
openai, anthropic, langchain-openai, langchain-anthropic, litellm and google-genai, sync and
async, streamed and not. The
support matrix is
generated from the SDK's own target declarations and lists, per library, whether usage is observable on
a streamed call and each one's caveat. A model request your code sends through its own httpx client
can be captured by wrapping that client's transport in hajer.CaptureTransport.
Documentation
- Reference — the public
API, the
verifyreason table, theobservedelivery contract, idempotency and case keys, whatwrapcaptures, streaming, redaction, attach mode, the command line, every setting, costs and errors. - CI suites — the pytest plugin that runs Hajer suites in your CI, and the GitHub Action that wraps it.
- Local platform —
pointing the SDK at a local or self-hosted Hajer platform, and a first
verifyyou can run as written. - Development — working on this package.
License
MIT.
Metadata
Release files for hajer 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hajer-0.1.0.tar.gz | 231.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hajer-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 505.9 kB
Release files / hajer-0.1.0.tar.gz
| Download URL | hajer-0.1.0.tar.gz |
|---|---|
| Size | 231.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7eef7e6efad7ee49b2465d25a966a4423c2f161add7a79ee49ed4c6d4a76264e
|
|
BLAKE2b-256 checksum How to use checksums |
4c6c8a40a843fdeec8d508d0796de412a060a5ebed3d41113ae9ce6b28d43bb5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 4, 2026.
Transparency logRelease files / hajer-0.1.0-py3-none-any.whl
| Download URL | hajer-0.1.0-py3-none-any.whl |
|---|---|
| Size | 274.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f0068226230103e79672ad8fa4beeceab9f64809f23363e318dfcd2e086a2d50
|
|
BLAKE2b-256 checksum How to use checksums |
f34e5e3328e905849ac9a51faad46aef025e9fd9927a4d3216081f012b024334
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 4, 2026.
Transparency log