hajer — the Python SDK
Your application's model calls, as traces the Hajer platform stores and shows: every request a trace, every
model call a generation under it, every conversation a session you can read top to bottom. And hajer eval:
the repository's promptfoo suites, run on a pinned engine in CI and reported to the platform against the
workflows and obligations they cover.
This package does not run your application, does not sit in your provider path and decides nothing on your behalf. It records what the application already does and exports it over OpenTelemetry.
Install
pip install "hajer[otel]" # the SDK with the OpenTelemetry exporter: what a production app installs
pip install hajer # httpx + pydantic only: records locally, exports nothing (no OTel SDK)
pip install "hajer[evals]" # `hajer eval`: run promptfoo suites on the pinned engine (Node >= 22.22 on PATH)
pip install "hajer[openai]" # with the OpenAI SDK alongside; "hajer[anthropic]" likewise
The openai and anthropic extras are a convenience: the SDK never imports either library. wrap(client)
instruments the object you hand it, by attribute. The otel extra is optional on purpose — this package
installs into your environment, and an application pinned to an older OpenTelemetry keeps its pin — but
without it nothing leaves the process, and hajer doctor says so.
Python ≥ 3.11, httpx>=0.28.1, pydantic>=2.12; with [otel], opentelemetry-sdk>=1.37 and the OTLP/HTTP
exporter.
Configure
Two environment variables, read once:
| Variable | Required | Meaning |
|---|---|---|
HAJER_API_KEY |
yes | Your team API key. |
HAJER_TEAM_ID |
yes | The team the traces belong 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.
Inert without a key. Without HAJER_API_KEY and HAJER_TEAM_ID — or with HAJER_DISABLED=1 — the SDK
is inert: every call is still recorded locally, nothing opens a socket, 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): it is on every span as
deployment.environment.name. Every other setting has a default; the full table is in the
reference.
Quickstart
import hajer
from openai import OpenAI
hajer.instrument() # every provider client built from here on records its model calls
client = OpenAI() # (or: client = hajer.wrap(OpenAI()) where you build it)
@hajer.workflow("answer-support-question") # the unit the platform shows a trace as
def handle_ticket(ticket):
with hajer.session(ticket.conversation_id), hajer.user(ticket.customer_id):
reply = client.chat.completions.create(
model="gpt-5", messages=[{"role": "user", "content": ticket.question}]
).choices[0].message.content
return reply
That is the integration. With the two variables set, each handle_ticket call is one trace on the platform:
a workflow answer-support-question span, a chat gpt-5 generation under it carrying the messages, the
answer, the model, the tokens and the finish reason, and session.id / user.id on both — so the
conversation reads as one thread across requests. Three things about those lines:
instrument()orwrap(client).wrapinstruments the client object you hand it, in place;instrument()patches the provider classes so clients built later are instrumented too. Either is enough.workflowis the root. A model call outside any workflow is still a trace, of one generation; inside one, it is a generation under the workflow span, andhajer.component/hajer.tooldescribe the steps between.sessionis the conversation. It,user,tagsandmetadata(hajer.context(...)) are declared once and carried by every span inside — the SDK's own and any other OpenTelemetry instrumentation's.
Concepts
What a model span carries. The GenAI semantic conventions' own attributes: gen_ai.operation.name,
gen_ai.provider.name, the request and response model, gen_ai.request.temperature and friends,
gen_ai.response.id and finish reasons, gen_ai.usage.input_tokens / output_tokens, and — with content
capture, which is on by default — gen_ai.input.messages, gen_ai.output.messages and
gen_ai.system_instructions in the conventions' message shape, whichever library made the call. Also the
innermost application frame (code.function.name, code.file.path, code.line.number), the provider's
host, and on failure error.type with an error status — never the message.
Where the spans go. With a key, to the platform's receiver for the team, as OTLP/HTTP; the exporter is
added to the OpenTelemetry provider the process already has, beside its own exporters, or to one the SDK
builds and never installs globally. HAJER_OTLP_ENDPOINT names a collector of your own instead. Export is
batched on its own thread and never blocks a call; an unreachable receiver costs at most one export timeout
at exit.
Client-side redaction is on by default. Card numbers, IBANs, national ids, credentials, email addresses
and similar shapes are replaced with [redacted:<CATEGORY>] in every message, answer and tool argument
before a span carries it. hajer.configure(policy=hajer.build_policy(...)) adjusts it.
Attach mode. For an application nobody has instrumented yet: every provider client the process builds records and exports its calls, with no code change. 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
Another instrumentation already there. The SDK's model span is not emitted for a call another
OpenTelemetry instrumentation already traces (its gen_ai span current around the call, or started inside
it), and HAJER_MODEL_SPANS=0 turns the SDK's model spans off outright. The session, user and workflow are
stamped onto that instrumentation's spans either way.
Evals: hajer eval
# hajer.yaml — the repository's suites and the obligations its tests cover
version: 1
suites:
- id: support
path: evals/support/promptfooconfig.yaml
obligations:
- id: obl_refund_status_disclosed
title: Refund status is disclosed accurately
workflow: wf_support
hajer eval # runs every declared suite on the pinned promptfoo; one payload per suite
hajer eval --upload # and reports each run to the platform, against the repository the commit belongs to
An ordinary promptfoo configuration, with metadata.hajer.workflowId and obligationIds on the tests that
cover an obligation. The platform reads the same hajer.yaml through the repository's GitHub connection, so
the suites, their tests and the obligations are there before the first run is uploaded.
docs/evals.md has the whole of it.
Command line
hajer doctor # every HAJER_* setting in force, where it came from, where spans would go, whether the service answers
hajer attach-path # the directory to put on PYTHONPATH for attach mode
hajer eval # run the repository's suites on the pinned engine (docs/evals.md)
hajer is a console script; python -m hajer is the same program. 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, what a span carries, what
wrapcaptures, streaming, redaction, attach mode, the command line, every setting, and errors. - Evals —
hajer eval,hajer.yaml, the payload; and the engine pin behind it. - Development — working on this package.
License
MIT.
Metadata
Release files for hajer 0.2.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.2.0.tar.gz | 267.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hajer-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 558.6 kB
Release files / hajer-0.2.0.tar.gz
| Download URL | hajer-0.2.0.tar.gz |
|---|---|
| Size | 267.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0913b5efb5693a51b4abbebfaa24d36baeae595d43066f1dd25a04a632dad526
|
|
BLAKE2b-256 checksum How to use checksums |
4c5f79b38c6271ad0b29ad7d1303910354c969dbbfa8af4a47fc40ad123ef824
|
| 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.2.0-py3-none-any.whl
| Download URL | hajer-0.2.0-py3-none-any.whl |
|---|---|
| Size | 291.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a15dc4bd7393893c9390c8cc32e91efdff9455b63fae2a0fe0610f1c96dedc5a
|
|
BLAKE2b-256 checksum How to use checksums |
6db378b0c04c4c69a9466e0aeef390a3eb3a1ba5edc6163b325e0097994ce6ae
|
| 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