zoroval
The Zoroval Python SDK. It sends your LLM app's traces to Zoroval, grouped as whole conversations, so you can find and fix what goes wrong for your users.
- One
init()line. OpenAI (Chat Completions and Responses) and Anthropic (Messages) are traced automatically: sync, async and streaming, with tool-call ids. - One trace per conversation: wrap a chat in
zoroval.conversation(chat_id). - Safe for production: it never raises into your code, never blocks a request, caps its memory in bytes, and exits within 5 seconds even when the network is down.
- Works beside Langfuse, OpenTelemetry, OpenInference and OpenLLMetry, in either install order, without duplicate spans.
- Apache-2.0.
Status: 0.1.0, not yet published. The gateway it talks to is being built.
Install and set up
pip install zoroval
zoroval login # opens the browser; writes ZOROVAL_INGEST_KEY to your git-ignored .env
zoroval doctor # sends a test trace and checks it arrived
Using a coding agent? Point it at get-started.md. The setup skill is in skills/setup/SKILL.md.
Use
import zoroval
from openai import OpenAI
zoroval.init() # reads ZOROVAL_INGEST_KEY
client = OpenAI()
def handle_message(chat_id: str, user_id: str, text: str) -> str:
with zoroval.conversation(chat_id, user_id=user_id):
with zoroval.turn(input=text) as turn:
reply = client.chat.completions.create(model="gpt-5", messages=history(chat_id, text))
answer = reply.choices[0].message.content
turn.set_output(answer)
return answer
Agents and batch jobs that are not chats use zoroval.trace():
@zoroval.tool
def search_orders(customer_id: str) -> list: ...
@zoroval.step
def plan(goal: str) -> list: ...
with zoroval.trace("nightly-report", user_id="ops"):
plan("summarize yesterday")
A @zoroval.tool run links to the model's tool-call id: pass call_id= to zoroval.tool(...), or let the SDK match the one pending call with the same name (and arguments, when several are pending). Ambiguous calls stay unlinked; the SDK never pairs by position.
API
| Call | What it does |
|---|---|
zoroval.init(api_key=None, *, service_name=None, environment=None, release=None, capture_content=True, redact=None, sample_rate=1.0, instrument=True, tracer_provider=None) |
Starts tracing. Safe to call twice. Returns False (and does nothing) without a valid key. |
zoroval.conversation(id, *, user_id=None, tags=None, metadata=None) |
Every turn inside joins one Zoroval trace. with or async with. |
zoroval.turn(name=None, *, input=None, ...) |
Root span of one user turn. |
zoroval.trace(name=None, *, input=None, user_id=None, tags=None, metadata=None) |
Root span of one run. Inside a conversation it acts as a turn. |
@zoroval.step, @zoroval.step(name=..., type="chain"), with zoroval.step("x"): |
A custom step; captures arguments and the return value (capped). |
@zoroval.tool, with zoroval.tool("name", call_id=..., arguments=...) as t: |
A tool run with its call id. |
zoroval.set_user(), set_tags(), set_metadata() |
On the current turn or trace, or on the current conversation. |
zoroval.flush(timeout=5.0) |
Send what is queued. Call it at the end of serverless handlers. |
zoroval.shutdown(timeout=5.0) |
Flush and stop. Runs at exit automatically. |
zoroval.status() |
Counters: queued, exported, dropped, failed, rejected, filtered, sampled out, internal errors, ungrouped turns. |
Environment variables: ZOROVAL_INGEST_KEY, ZOROVAL_INGEST_URL, ZOROVAL_SERVICE_NAME (or OTEL_SERVICE_NAME), ZOROVAL_ENVIRONMENT, ZOROVAL_RELEASE, ZOROVAL_CAPTURE_CONTENT, ZOROVAL_SAMPLE_RATE, ZOROVAL_ENABLED.
Privacy
- Prompts and outputs are captured by default.
capture_content=Falsekeeps model names, usage, timings, errors and tool-call ids, and drops content. redact=fngets each captured value and returns the value to send. Iffnraises, that field is dropped, never sent raw. Changed spans carryzoroval.redacted=true.- Both controls also cover spans from other tools (OpenInference, Langfuse, OpenLLMetry, OTel GenAI). Their content attributes (for example
input.value,llm.input_messages.*,langfuse.observation.input,gen_ai.prompt.*,gen_ai.input.messages) are dropped, or passed throughredactas the raw attribute value, and GenAI span events are dropped. Your other exporters still get the original spans. - GenAI log events from the OTel contrib instrumentors are sent only when
capture_content=Trueand noredacthook is set. @zoroval.stepand@zoroval.toolcapture all arguments and the return value. Usecapture_input=Falseon functions that take secrets, orcapture_content=False.- Images and files in messages are replaced by placeholders.
- Only GenAI spans leave the process. Your app's HTTP, database and other spans are never sent to Zoroval.
- When Zoroval attaches to your existing OpenTelemetry provider, its LLM spans also reach your other exporters, with content. Use
capture_content=Falseor a filter on that side if needed.
How it behaves under load and failure
| Situation | Behavior |
|---|---|
| Gateway slow or down | Your requests are not slowed. Spans wait in a queue capped at 64 MiB (max_queue_bytes) and 20,000 spans; the oldest are dropped first and counted. The next accepted request reports the count in X-Zoroval-Dropped-Spans. |
| Process exit with the gateway down | Exit waits at most shutdown_timeout (5 s). |
| Large prompts | Each content field is capped at 4 MiB (max_content_bytes), cut so the JSON stays valid, with zoroval.truncated_bytes set. Metadata is capped at 16 KB. |
| Batches | Cut by bytes (16 MiB raw), zstd level 3, split again if the compressed body passes the 16 MiB gateway limit. Retries 429 and 503 after Retry-After, and 5xx with backoff, reusing the batch id. |
| Invalid or revoked key | Export stops for the process; zoroval.status()["key_refused"] says why. |
| gunicorn, uvicorn workers, Celery prefork | The export thread restarts in each forked worker. The parent's queued spans are not sent twice. |
| Serverless | Call zoroval.flush() at the end of each handler. |
| Head sampling | sample_rate=0.2 keeps 20% of conversations, each kept whole. |
Coexistence
| You already use | What init() does |
|---|---|
Your own OpenTelemetry TracerProvider |
Adds Zoroval's processor to it. If you set yours after init(), your processors are attached to the global provider so both keep receiving spans. |
Langfuse v3+ (@observe, langfuse.openai) |
Attaches to Langfuse's provider and reads its generation spans; does not add a second OpenAI span. Langfuse's session_id groups conversations. |
| OpenInference, OpenLLMetry, OTel contrib instrumentors | Reads their spans; does not wrap a client they already wrap. GenAI log events from the contrib instrumentors are sent too. |
| The Anthropic client's built-in OpenTelemetry span | Not sent twice: Zoroval's generation span covers the call. |
Development
uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python -e '.[dev]' openinference-instrumentation-openai python-dotenv
.venv/bin/python -m pytest
Tests run against a fake Zoroval gateway (testing/fake_gateway.py) and fake OpenAI and Anthropic servers (testing/fake_llm.py). No real API calls.
License
Apache-2.0. See LICENSE.
Metadata
Release files for zoroval 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 | Provenance | Actions |
|---|---|---|---|---|
| zoroval-0.1.0.tar.gz | 106.3 kB | With provenance | Details for zoroval-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| zoroval-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for zoroval-0.1.0-py3-none-any.whl |
Total release size: 180.0 kB
Release files / zoroval-0.1.0.tar.gz
| Download URL | zoroval-0.1.0.tar.gz |
|---|---|
| Size | 106.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
15e4e3789f75fd3e193952137a2293d6056d6e8d4e68fa83781c3bb48857d2f9
|
|
BLAKE2b-256 checksum How to use checksums |
a8f6e3646c8e17f2f92313bdb8480fe3f00b9a8fcc3939f4bee844e057345802
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Nov 10, 2026.
Transparency logRelease files / zoroval-0.1.0-py3-none-any.whl
| Download URL | zoroval-0.1.0-py3-none-any.whl |
|---|---|
| Size | 73.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
651642de204908feb28e6d66643e5fcc9e87b5ee34039c30866377f3bb415adf
|
|
BLAKE2b-256 checksum How to use checksums |
7c1d13de1f604a18c27b46e6a4045ce1d1bb5410ab2d5af4e1db928f919cc253
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Nov 10, 2026.
Transparency logSecurity for zoroval 0.1.0
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.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for zoroval-0.1.0.tar.gz
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 Nov 10, 2026.
Transparency logA 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.