Skip to main content

LLM regression testing via embedding drift detection

Project description

litmus-sdk

LLM regression testing via embedding drift detection.

Litmus instruments your LiteLLM calls passively, stores traces, and uses embedding-space distance to detect whether your LLM outputs have semantically drifted between versions — not just whether they match character-for-character.


Installation

pip install litmus-sdk

With optional providers:

pip install litmus-sdk[openai]           # OpenAI embedding provider
pip install litmus-sdk[chromadb]         # ChromaDB vector storage
pip install litmus-sdk[openai,chromadb]  # both

Quickstart

Passive instrumentation

import litellm
from litmus_sdk import LitmusSDK

sdk = LitmusSDK(db_path="./litmus.db", project_id="my-project")
sdk.init("v1.0.0")

# All litellm.completion() calls are now automatically captured
response = litellm.completion(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Summarise this article: ..."}]
)

sdk.init("v2.0.0")
# ... more calls with a different prompt or model ...

report = sdk.compare("v1.0.0", "v2.0.0")
report.display()
sdk.close()

With GoldenTestRunner

import litellm
from litmus_sdk import LitmusSDK, GoldenTestRunner

def my_agent(question: str) -> str:
    resp = litellm.completion(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": question}]
    )
    return resp.choices[0].message.content

with LitmusSDK(project_id="qa-bot") as sdk:
    sdk.init("v1.0.0")
    runner = GoldenTestRunner(sdk)

    runner.add_test_case(
        name="capital_cities",
        inputs={"question": "What is the capital of France?"},
        run_function=my_agent,
        expected_pattern=r"Paris",
    )

    report = runner.run_tests(num_runs=5)
    report.display()

    sdk.init("v2.0.0")
    runner.run_tests(num_runs=5)

    drift = sdk.compare("v1.0.0", "v2.0.0")
    drift.display()

Environment variables

OPENAI_API_KEY=sk-...              # required for embedding computation
LITMUS_DB_PATH=./litmus.db         # SQLite file path (created automatically)
LITMUS_CHROMA_PATH=./chroma        # ChromaDB directory (defaults to ./chroma next to DB)

API Reference

LitmusSDK

The main entry point.

sdk = LitmusSDK(
    db_path="./litmus.db",     # path to SQLite file
    chroma_path="./chroma",    # path to ChromaDB directory (optional)
    project_id="my-project",   # tags all traces (optional)
)
Method Description
init(version) Sets the active version and registers the LiteLLM callback. Returns self for chaining.
get_traces(version) Returns all LLMTrace objects for the given version.
get_versions() Returns all version strings in insertion order.
compare(version_a, version_b) Runs drift detection and returns a DriftReport. Triggers embedding backfill on first call.
close() Deregisters the callback and closes the DB.

Supports context manager: with LitmusSDK() as sdk: calls close() automatically.


GoldenTestRunner

Executes test cases multiple times to measure stability and populate the drift pipeline.

runner = GoldenTestRunner(sdk)
Method Description
add_test_case(name, inputs, run_function, expected_pattern, expected_behavior) Registers a test case. run_function is called with **inputs on each run.
run_tests(num_runs) Runs all registered tests num_runs times. Returns a TestReport.

DriftDetector

Can be used directly for more control over drift comparison.

from litmus_sdk.drift import DriftDetector

detector = DriftDetector(sdk, drift_threshold=0.10)
report = detector.compare("v1.0.0", "v2.0.0")

The drift_threshold sets the sensitivity for flagging individual tests (cosine distance). For full control over classification thresholds, see DriftConfig below.


DriftConfig — classification thresholds

Attribute Default Meaning
threshold_safe 0.05 Output drift below this → SAFE
threshold_review 0.15 Below this → REVIEW
threshold_warning 0.30 Below this → WARNING, at or above → REGRESSION
stability_min_safe 0.90 Output stability must be ≥ this for SAFE
stability_min_review 0.80 Output stability must be ≥ this for REVIEW

DriftReport

Returned by sdk.compare() and detector.compare().

Field Type Description
version_a / version_b str The two versions being compared
avg_input_drift float Mean cosine distance of prompt embeddings [0, 2]
avg_output_drift float Mean cosine distance of response embeddings [0, 2]
stability_a / stability_b float Output consistency per version [0, 1]
flagged_tests list[str] Test names that exceeded the drift threshold
recommendation str One of SAFE, REVIEW, WARNING, REGRESSION
details str Human-readable explanation

Call report.display() to pretty-print with status icons.


LitmusCallback

The LiteLLM callback that captures traces. Registered automatically by sdk.init() — you typically don't need to use it directly. If you do need custom integration:

from litmus_sdk.callback import LitmusCallback

callback = LitmusCallback(sdk)
litellm.callbacks = [callback]

Data models

Model Description
LLMTrace One captured LLM call: prompt, response, model, tokens, latency, cost, embeddings
PromptComponents Decomposed prompt: system_prompt, user_input, context, final_prompt
TestCase A golden test definition with name, inputs, run_function, expected_pattern
TestResult One test execution: output, passed, latency_ms, error
TestReport Full suite result with per-test summaries and pass rates

How it works

litellm.completion()
        │
        ▼
LitmusCallback (LiteLLM hook)
  - Extracts prompt + response
  - Stores metadata → SQLite
  - Computes embeddings eagerly (OpenAI API call)
  - Stores vectors → ChromaDB
        │
        ▼
sdk.compare("v1", "v2")
        │
        ▼
DriftDetector
  1. Load traces for both versions
  2. Compute centroids of embeddings + cosine distances
  3. Classify: SAFE / REVIEW / WARNING / REGRESSION
  4. Return DriftReport

Embeddings are computed eagerly during callback capture (via OpenAI API, ~50-200 ms per call) so that drift detection works immediately without additional setup. Embedding computation happens asynchronously in the LiteLLM callback hook, so it doesn't block your agent's latency.

Hybrid storage — SQLite stores structured metadata (fast queries, no infrastructure). ChromaDB stores vectors (optimized cosine distance). Both are linked by trace_id.


Storage backends

Default: SQLite + ChromaDB, both local files.

Env var Default Purpose
LITMUS_DB_PATH ./litmus.db SQLite file
LITMUS_CHROMA_PATH ./chroma (next to DB) ChromaDB directory

SQLite uses WAL mode for thread safety.


Embedding providers

Default: text-embedding-3-small via OpenAI.

To swap the provider, subclass EmbeddingProvider and pass it to LitmusConfig:

from litmus_sdk.embeddings.base import EmbeddingProvider

class MyProvider(EmbeddingProvider):
    def embed(self, text: str) -> list[float]: ...
    def embed_batch(self, texts: list[str]) -> list[list[float]]: ...
    @property
    def dimensions(self) -> int: return 1536

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

litmus_sdk-0.1.3.tar.gz (56.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

litmus_sdk-0.1.3-py3-none-any.whl (66.3 kB view details)

Uploaded Python 3

File details

Details for the file litmus_sdk-0.1.3.tar.gz.

File metadata

  • Download URL: litmus_sdk-0.1.3.tar.gz
  • Upload date:
  • Size: 56.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for litmus_sdk-0.1.3.tar.gz
Algorithm Hash digest
SHA256 edba1e7c29be622b6f4dab6fa004472dbe2a2ae2a1b50051d6724c3415595637
MD5 51008fe85f10aff7da0b4922be710a14
BLAKE2b-256 159bca731698838d49fe9f9b7785214c486d1de78e7b86ec1e73799692da9358

See more details on using hashes here.

File details

Details for the file litmus_sdk-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: litmus_sdk-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 66.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for litmus_sdk-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 b154bf1f8e341358451e172e3be820d423ecefccfc92ffc4eb3b19f7d74874d2
MD5 9b7d252fc277b52ebee098e31e104d20
BLAKE2b-256 f977d3ad2f1550c48c848eedfbc064a615372dcd0a53fe7c9cccb7167101760c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page