Official Python SDK for LLMJury — run LLM experiments in production: deterministic variant assignment, prompt/model comparison, and non-blocking metrics tracking.
Project description
LLMJury Python SDK
The official Python SDK for LLMJury — run LLM experiments in production.
LLMJury lets you compare prompts and models on real traffic, with real users, and measure what actually matters: your business outcomes. The SDK assigns each user to an experiment variant deterministically, captures latency / tokens / errors from your existing LLM calls automatically, and streams everything to the LLMJury dashboard where the stats engine tells you which variant wins — and when you have enough data to trust it.
Why teams use it:
- Ship prompt and model changes like feature flags. Roll a new prompt to 10% of traffic, watch the metrics, roll forward or back — no redeploy.
- Decide on outcomes, not vibes. Tie each variant to conversion, retention, resolution rate — whatever your business metric is — with statistical rigor built in.
- Zero overhead on the hot path. Variant assignment is pure local compute (no network call), and tracking never blocks and never throws into your app. A full LLMJury outage degrades to your in-code defaults.
Installation
pip install llmjury-sdk
Python 3.8+. Zero runtime dependencies — the SDK uses only the standard library.
Authentication
Get your publishable API key from the LLMJury dashboard (Settings → API keys, it looks
like llmj_pk_...) and export it:
export LLMJURY_API_KEY=llmj_pk_...
The publishable key is write-only and rate-limited, so it is safe to ship in any environment.
The SDK reads it automatically; you can also pass Client(api_key="llmj_pk_...") explicitly.
Quick start
Create an experiment in the dashboard (say, checkout-prompt, with variants control and
friendly, each carrying a prompt). Then:
from llmjury import Client
# Once, at startup. Prefetching means the first assign resolves instantly.
client = Client(experiments=["checkout-prompt"])
# Per request: which variant is this user in, and what prompt does it carry?
p = client.get_prompt("checkout-prompt", user_id, default="You are a helpful assistant.")
print(p.variant, p.prompt) # e.g. "friendly", "You are a warm, upbeat shopping guide..."
# When the user converts, record the outcome — this is what the experiment is measured on.
client.track("business_event", {
"experiment_id": "checkout-prompt",
"user_id": user_id,
"variant": p.variant,
"business_metric": "conversion",
"value": 1,
})
That's the whole loop: assign → use the variant's prompt → track the outcome. The dashboard does the rest.
Recommended production setup
Add the setup-once wrap() interceptor and the SDK also captures latency, token usage, model
name, errors, and time-to-first-token from every LLM call — with zero per-call code:
from llmjury import Client
import anthropic
# ---- once, at startup -------------------------------------------------------
client = Client(experiments=["checkout-prompt"])
llm = client.wrap(anthropic.Anthropic(), "checkout-prompt") # every model call is now traced
# ---- per request ------------------------------------------------------------
with client.as_user(user_id):
p = client.get_prompt("checkout-prompt", user_id, default="You are a helpful assistant.")
# Call your provider client DIRECTLY — metrics are intercepted automatically.
response = llm.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
system=p.prompt,
messages=[{"role": "user", "content": user_input}],
)
# The ONLY explicit tracking you write is the business outcome:
client.track("business_event", {
"experiment_id": "checkout-prompt", "user_id": user_id,
"variant": p.variant, "business_metric": "conversion", "value": 1,
})
The wrapper is a duck-typed proxy — it works with OpenAI- and Anthropic-style clients (or anything shaped like them) without importing any provider SDK.
Core concepts
| Concept | What it is |
|---|---|
| Experiment | A named test with an ordered list of variants and traffic weights, defined in the dashboard. Address it by id or unique name. |
| Variant | One arm of the experiment. Carries a prompt and/or arbitrary variables (e.g. model, temperature). |
| Assignment | assign(experiment, user) → variant key. Deterministic: the same user always gets the same variant, in every SDK language, with no network call. |
| User / session | Any stable string id you choose — user id, session id, tenant id. It is hashed, never stored raw for assignment. |
| Exposure | "User X saw variant Y" — recorded automatically by wrap/interception, or track it yourself. |
| Model call | One LLM invocation: latency, tokens in/out, model, error — captured by the interceptor. |
| Business event | Your outcome metric (conversion, thumbs-up, resolution). The thing the experiment is judged on. |
Comparing models
Variants can carry variables, so an experiment can vary the model (or temperature, or any knob) instead of — or alongside — the prompt:
v = client.get_variables("model-shootout", user_id,
defaults={"model": "claude-haiku-4-5", "temperature": "0.3"})
response = llm.messages.create(model=v.values["model"], ...)
Comparing prompts
get_prompt is the purpose-built path: each variant carries a prompt, and your in-code default
is the guaranteed fallback. Rich prompt workflows (versioning, edit history, per-variant prompt
text) are managed in the dashboard.
Manual control
The low-level primitives are always available if you want full control:
variant = client.assign("checkout-prompt", user_id) # just the variant key (or None pre-config)
client.track("exposure", {"experiment_id": "checkout-prompt", "user_id": user_id, "variant": variant})
client.track("model_call", {"experiment_id": "checkout-prompt", "user_id": user_id,
"variant": variant, "latency_ms": 840, "tokens_input": 512,
"tokens_output": 128, "model": "claude-sonnet-5"})
Async variants exist for both: await client.aassign(...), await client.atrack(...).
Error handling & resilience
The SDK is engineered so that LLMJury can never take your app down:
assign/get_prompt/get_variablesnever touch the network on the hot path — they compute against a locally cached config that refreshes in the background (60s polling,ETag/304).trackappends to a bounded in-memory buffer and returns immediately. A background thread flushes every second (or every 1,000 events).- Failed flushes retry a bounded number of times, then spill to disk (if
offline_pathis set) and replay later with their original timestamps (bounded to 24h), or drop. They never raise into your code. - Before the first config fetch completes,
assignreturnsNoneandget_promptreturns yourdefault— your app keeps working during a cold start with the network down.
There are no exceptions to catch. The one deliberate consequence: telemetry is best-effort — if
your process is killed without client.close(), buffered events from the last flush interval may
be lost.
Configuration
Client(
api_key=None, # default: $LLMJURY_API_KEY
base_url=None, # default: $LLMJURY_BASE_URL, else https://api.llmjury.com
experiments=None, # experiment ids/names to prefetch at startup
flush_interval=1.0, # seconds between background flushes
flush_size=1000, # flush early when the buffer reaches this size
config_poll_interval=60.0, # seconds between config refreshes
offline_path=None, # file path enabling offline spill/replay
request_timeout=5.0, # per-request timeout, seconds
)
Lifecycle: construct one Client per process and reuse it. Call client.flush() to drain
synchronously (e.g. at the end of a batch job) and client.close() at shutdown.
Examples
Runnable scripts live in examples/:
quickstart.py— assign + track end-to-end (runs offline, no account needed)prompt_comparison.py— A/B test two prompts withget_promptmodel_comparison.py— route traffic across models withget_variablesproduction_integration.py— the fullwrap()+ business-outcome pattern
The determinism guarantee
Assignment is a frozen, cross-language contract: MurmurHash3 over salt:user:experiment with
integer-only boundary arithmetic, specified in spec/bucketing.md. The
Python, TypeScript, and
Java SDKs all assert the same conformance fixture in
CI, so a user gets the same variant no matter which service — in which language — asks.
Links
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file llmjury_sdk-0.1.0.tar.gz.
File metadata
- Download URL: llmjury_sdk-0.1.0.tar.gz
- Upload date:
- Size: 33.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1cb26ef968baed23056ff40ebcb3475b1a1669d005e9da273bd5e5ed766efe68
|
|
| MD5 |
f52d9f544011c3ebb6f637ffa5a0371c
|
|
| BLAKE2b-256 |
489ee1e281ea1b70e35ef7bc4c2bb9a7245cf67b0505a50bf5fedb6850be54c5
|
Provenance
The following attestation bundles were made for llmjury_sdk-0.1.0.tar.gz:
Publisher:
release.yml on llmjury/llmjury-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
llmjury_sdk-0.1.0.tar.gz -
Subject digest:
1cb26ef968baed23056ff40ebcb3475b1a1669d005e9da273bd5e5ed766efe68 - Sigstore transparency entry: 2188476954
- Sigstore integration time:
-
Permalink:
llmjury/llmjury-python@a4b270224b094ebac138d185f2c009da1e173e5c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/llmjury
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a4b270224b094ebac138d185f2c009da1e173e5c -
Trigger Event:
release
-
Statement type:
File details
Details for the file llmjury_sdk-0.1.0-py3-none-any.whl.
File metadata
- Download URL: llmjury_sdk-0.1.0-py3-none-any.whl
- Upload date:
- Size: 27.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
76d1f3f799cd72d722b1bb9d0efa26ebb06b32d9326145590141f9a675760428
|
|
| MD5 |
15e0b8ed90af5fc88be21ddf027b2b11
|
|
| BLAKE2b-256 |
f04efe569d87590e44e1616be65d2ad1136369789a725c5ffa05ba3c4a05f7c7
|
Provenance
The following attestation bundles were made for llmjury_sdk-0.1.0-py3-none-any.whl:
Publisher:
release.yml on llmjury/llmjury-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
llmjury_sdk-0.1.0-py3-none-any.whl -
Subject digest:
76d1f3f799cd72d722b1bb9d0efa26ebb06b32d9326145590141f9a675760428 - Sigstore transparency entry: 2188476962
- Sigstore integration time:
-
Permalink:
llmjury/llmjury-python@a4b270224b094ebac138d185f2c009da1e173e5c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/llmjury
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a4b270224b094ebac138d185f2c009da1e173e5c -
Trigger Event:
release
-
Statement type: