Skip to main content

CipherExplain Python SDK

Register your own models and get encrypted SHAP explanations via the CipherExplain API.

PyPI Python License: AGPL v3 Downloads

Install

pip install cipherexplain

Dependencies installed automatically: httpx, numpy, scikit-learn.

Optional: local FHE mode

For client-side CKKS encryption (fhe_mode='ckks') install the fhe extra:

pip install 'cipherexplain[fhe]'

This adds OpenFHE so the SDK can encrypt inputs locally and decrypt results without the server ever seeing plaintext.

Quick start

from cipherexplain_sdk import CipherExplainClient, extract_spec

client = CipherExplainClient(api_key="vb_...")

# 1. Train locally — nothing leaves your machine
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
model  = LogisticRegression().fit(scaler.fit_transform(X_train), y_train)

# 2. Extract weights only (no pickle, no training data)
spec = extract_spec(model, "my_model", feature_names=["age", "income", "score"],
                    scaler=scaler)

# 3. Register with the API
client.register(spec)

# 4. Get SHAP explanations — pass raw values, scaler is applied server-side
result = client.explain_raw("my_model", x_raw)
print(result["shap_values"])    # per-feature attributions
print(result["prediction"])     # 0.0–1.0 probability

Counterfactuals + ECOA Reg-B reason codes (v0.5.1)

For credit-denial / adverse-action workflows: after /explain, ask the server for a SHAP-guided minimal-L2 counterfactual + Form C-1 reason codes. The server constructs Enc(x') = Enc(x) ⊕ Enc(δ*) from its own session-stored Enc(x); the client never gets to submit Enc(x') directly, so the forged-CF attack is closed at the schema layer.

exp = client.explain_raw("my_lr", x_denied)
cf  = client.counterfactual("my_lr", x_denied, exp)

print(cf["x_prime"])              # x + δ* — decision-flipping recourse
print(cf["decision_flipped"])     # True
for c in cf["reason_codes"]:
    print(c["form_c1_code"], "—", c["form_c1_text"])

# Optional: protect class / sticky features
from cipherexplain_sdk import FeatureManifest
manifest = FeatureManifest(immutable=[True, False, ..., False])  # age fixed
cf = client.counterfactual("my_lr", x_denied, exp,
                            feature_manifest=manifest)

cf_attestation_mode is "UNATTESTED" by default. The Pedersen homomorphism

  • real BLS12-381 G1 Σ-IPA π_CF + CRDC audit chain + composition β are real (verified on EU production cloud infrastructure in 0.5 ms / 0.8 ms p50). For the lattice-arm attestation upgrade ("ATTESTED_BGV_ZK"), see the next section.

v1-B lattice attestation: bgv_zk=True (v0.6.0)

counterfactual(bgv_zk=True) attaches a del Pino-Lyubashevsky-Seiler (PKC 2019) lattice binding proof to the request. The SDK generates a fresh BGV keypair per call, encrypts the integer-quantised attribution vector δ_int into ct_BGV (at n'=512, q≈2^60), and runs the Σ-protocol locally; the server verifies the lattice-arm equation in ~50 ms and upgrades cf_attestation_mode from "UNATTESTED" to "ATTESTED_BGV_ZK".

cf = client.counterfactual("my_lr", x_denied, exp, bgv_zk=True)
assert cf["cf_attestation_mode"] == "ATTESTED_BGV_ZK"

Honest-client assumption (important): bgv_zk=True requires that ct_CKKS (used by the SHAP computation) and ct_BGV (used by the proof) encode the same δ_int. This is cryptographically unenforceable per scheme-switching hardness (eprint 2023/988). The threat model is adversarial-server output, honest client — appropriate for B2B internal attestation where the client and the protected party are the same entity (a bank attesting its own SHAP to an internal auditor or third party). For consumer-facing Reg-B / ECOA adverse-action use cases, stay on the default bgv_zk=False — the v1-A G1 Σ-IPA path is the right fit there.

Server-side, the bgv_zk payload is consumed only when the deployment sets CE_CF_USE_BGV_ZK=1. When the flag is off (default during rollout), the SDK can still send bgv_zk=True payloads — the server accepts but ignores them and the response stays on the v1-A path. No client changes are needed when the server flips the flag.

Cost profile: ~16.5 KB proof on the wire (well under the 50 KB budget), ~100 ms prover latency on Mac M1 (pure-Python reference; LaZer C extension on Linux AMD64 + AVX-512 reduces this 50-100× when bundled).

Supported model types

Tree ensembles (new)

RandomForestClassifier, GradientBoostingClassifier, DecisionTreeClassifier — and their regressor variants — are now fully supported. Use serialize_model from cipherexplain_eval to build the registration payload:

from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.preprocessing import StandardScaler
import cipherexplain_eval as ce

# Train locally
rf = RandomForestClassifier(n_estimators=100).fit(X_train, y_train)

# Serialize to JSON (no pickle, no training data)
payload = ce.serialize_model(
    rf,
    feature_names=["age", "income", "score"],
    model_id="my_rf",
)

# Register with the API (use the raw API or SDK client)
client.register(payload)

# Explain — fhe_mode is ignored for tree models, plaintext KernelSHAP runs
result = client.explain("my_rf", x)
print(result["shap_values"])

GradientBoostingClassifier works the same way — serialize_model captures the learning rate and initial estimator needed for correct probability reconstruction.

Linear classifiers

Weights serialised to JSON — model must be described by a coefficient matrix and intercept vector.

Framework How
sklearn LogisticRegression extract_spec(model, ...)
sklearn LinearSVC extract_spec(model, ...)
Any sklearn-compatible object with coef_ / intercept_ extract_spec(model, ...)
PyTorch nn.Linear extract_spec(layer, ...)
PyTorch pure-linear nn.Sequential extract_spec(seq, ...)
TensorFlow / Keras Dense (no activation) from_weights(layer.get_weights()[0].T, layer.get_weights()[1], ...)
JAX / statsmodels / R / anything else from_weights(coef, intercept, ...)

All supported model types

Supported: logistic regression, linear SVM, RandomForest, GradientBoosting, DecisionTree, and MLP (ReLU, 2-3 hidden layers). Any framework via weight export.

model_type Class SHAP method
logistic_regression sklearn.LogisticRegression KernelSHAP (FHE-simulated)
linear_svc sklearn.LinearSVC KernelSHAP (FHE-simulated)
decision_tree sklearn.DecisionTreeClassifier/Regressor KernelSHAP (plaintext)
random_forest sklearn.RandomForestClassifier/Regressor KernelSHAP (plaintext)
gradient_boosting sklearn.GradientBoostingClassifier/Regressor KernelSHAP (plaintext)
mlp (PANCE) sklearn.MLPClassifier (ReLU, 2-3 hidden layers) KernelSHAP via LP-optimal polynomial activation

TensorFlow / Keras example

from cipherexplain_sdk import from_weights

# Keras model with a single Dense output layer
import numpy as np
w, b = keras_model.layers[-1].get_weights()  # w shape: (n_features, n_classes)
spec = from_weights(
    coef=w.T,          # transpose to (n_classes, n_features)
    intercept=b,
    model_id="keras_model",
    feature_names=[...],
    classes=[0, 1],
)
client.register(spec)

JAX / statsmodels / R example

from cipherexplain_sdk import from_weights

# Pass coefficient array and intercept directly
spec = from_weights(
    coef=[[0.42, -0.17, 0.93]],   # shape (1, n_features) for binary
    intercept=[-0.31],
    model_id="my_model",
    feature_names=["f1", "f2", "f3"],
    classes=[0, 1],
    scaler_mean=[0.0, 0.0, 0.0],  # optional: embed scaler for /explain_raw
    scaler_scale=[1.0, 1.0, 1.0],
)
client.register(spec)

Client reference

client = CipherExplainClient(api_key="vb_...", base_url="https://cipherexplain.vaultbytes.com")

Model management

Method Description
client.register(spec) Register a model (spec from extract_spec or from_weights)
client.list_models() List all models available to your key
client.delete(model_id) Delete a registered model and free its slot

Slot limits: free = 1 model · developer = 10 · enterprise = unlimited

Explanations

Method Description
client.explain(model_id, features) SHAP explanation — features must be pre-scaled
client.explain_raw(model_id, features) SHAP explanation — raw values, auto-scaled server-side

Both return:

{
    "prediction": 0.74,          # model probability
    "base_rate": 0.31,           # baseline (training set average)
    "shap_values": [...],        # per-feature attribution (sum ≈ prediction - base_rate)
    "feature_names": [...],
    "metadata": {...}
}

MLP fast lane — linear_surrogate=True

For MLP models only, opt in per-request to a rank-1 Jacobian linear surrogate evaluated as a single FHE matmul against the registered baseline:

result = client.explain("my_mlp", x, fhe_mode="ckks", linear_surrogate=True)
print(result["metadata"]["method"])  # "openfhe_ckks_mlp_jacobian_surrogate"
Path Wall (EU production cloud) SHAP L∞ vs exact
Default (diagonal-encoded coalition-packed, d=50, K=390) ~73 s 0 (exact path)
linear_surrogate=True ~7 s 0.062 measured · error_bound=0.15 reported

The 0.15 reported error_bound is the conservative empirical floor we sign customer SLAs against. No-op for non-MLP models. If your accuracy budget is tighter than 0.15, omit the flag and accept the ~73 s wall.

Account

Method Description
client.usage() Monthly quota: used / remaining
client.rotate_key() Issue new key, deactivate current. Models migrate automatically.
client.health() API health check — no key required
client.load_demo_model() Load the demo credit model (credit_model)

Key rotation

result = client.rotate_key()
print(result["new_key"])         # vb_... — save this immediately
print(result["models_migrated"]) # all your models move automatically

# Your old key is now inactive — create a new client
client = CipherExplainClient(api_key=result["new_key"])

Error handling

All methods raise httpx.HTTPStatusError on API errors. Check exc.response.status_code:

import httpx

try:
    client.register(spec)
except httpx.HTTPStatusError as exc:
    if exc.response.status_code == 401:
        print("Missing X-API-Key")
    elif exc.response.status_code == 403:
        print("Invalid key or tier restriction (e.g. PDF reports need Developer+)")
    elif exc.response.status_code == 404:
        print("Model not found")
    elif exc.response.status_code == 409:
        print("Model ID already registered — delete it first or use a different ID")
    elif exc.response.status_code == 422:
        print("Validation error:", exc.response.json()["detail"])
    elif exc.response.status_code == 429:
        print("Quota exceeded — model slot limit or monthly call limit reached")
    else:
        raise

Differential Privacy for Published SHAP Values

Use DP only when you publish SHAP values externally (dashboards, reports, partners). For internal use inside a trust boundary it is unnecessary.

Via the API — apply_dp=True

# Enable DP protection before sharing SHAP values externally
result = client.explain_raw(
    "my_model",
    x_raw,
    apply_dp=True,
    dp_epsilon=10.0,
    dp_delta=1e-5,
    dp_clip_C=0.2,
)
print(result["shap_values"])        # DP-protected values
print(result["dp_sigma"])           # noise std used
print(result["dp_epsilon_used"])    # epsilon applied

# WARNING: only enable apply_dp=True when sharing SHAP values
# with external parties. If using SHAP values internally only,
# FHE already protects the input — DP is not needed.

Client-side — bring your own phi

from cipherexplain_sdk import clip_and_noise, PrivacyAccountant

# phi_hat is the decrypted post-regression SHAP vector (length d).
phi_noisy, sigma = clip_and_noise(phi_hat, C=0.2, epsilon=10.0, delta=1e-5)

acct = PrivacyAccountant(epsilon_per_query=10.0, delta=1e-5)
acct.record_query()
print(acct.total_epsilon(), acct.total_delta())

Recommended parameters: C=0.2, epsilon=10-20, delta=1e-5.

Warnings:

  • Do not re-normalize phi_noisy (e.g. by dividing so the values sum to y_hat - baseline). Re-normalization multiplies noise by an input-dependent factor and breaks DP. The efficiency axiom holds in expectation via zero-mean noise.
  • Noise is calibrated against the post-regression SHAP sensitivity, which is Theta(1) in K (Paper 4 column-cancellation lemma), not the pre-regression Theta(sqrt(K)/n).
  • Compose budget across queries via PrivacyAccountant (zCDP accounting).

Tier limits

Free Developer Enterprise
SHAP API calls / month 50 5,000 50,000
Oracle runs / month 3 500 5,000
Model slots 1 10 Unlimited
PDF audit reports — Yes Yes

Get an API key

vaultbytes.com/cipherexplain

Metadata

Release files for cipherexplain 0.8.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cipherexplain 0.8.1
File Size Uploaded Provenance Actions
cipherexplain-0.8.1.tar.gz 126.3 kB With provenance Details for cipherexplain-0.8.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for cipherexplain 0.8.1
File Interpreter ABI Platform Actions
cipherexplain-0.8.1-py3-none-any.whl Python 3 none any Details for cipherexplain-0.8.1-py3-none-any.whl

Total release size: 271.8 kB

Release files / cipherexplain-0.8.1.tar.gz

Download URL cipherexplain-0.8.1.tar.gz
Size 126.3 kB
Tags Source
SHA-256 checksum
How to use checksums
7c21c935a260eb576781cadda1eb17586dafbe68dfa702ff92e8d8ad9b6433e2
BLAKE2b-256 checksum
How to use checksums
14347c7a7cebcf0091456ec8d5df25c09810cd4ff4eb569f9d0a8f06ad6e3a26
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

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 Jul 19, 2026.

Transparency log

Release files / cipherexplain-0.8.1-py3-none-any.whl

Download URL cipherexplain-0.8.1-py3-none-any.whl
Size 145.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8d3b6787577e465494000dece0931d52638a1487e9eda2247264354cc588a0d3
BLAKE2b-256 checksum
How to use checksums
d499e8bfaab7eb5b29671bdbfc5d75f2a61381c8b58649dfba8d8fdefc2b3d1d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

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 Jul 19, 2026.

Transparency log

Security for cipherexplain 0.8.1

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

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for cipherexplain-0.8.1.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 Jul 19, 2026.

Transparency log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page